Skip to content

Commit 7426fcc

Browse files
authored
Merge pull request #1237 from godot-rust/qol/match-class-simplification
Simplify `match_class!` syntax + implementation
2 parents 67a4dfe + f3513a2 commit 7426fcc

File tree

2 files changed

+77
-57
lines changed

2 files changed

+77
-57
lines changed

godot-core/src/classes/match_class.rs

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
///
1414
/// Requires a fallback branch, even if all direct known classes are handled. The reason for this is that there may be other subclasses which
1515
/// are not statically known by godot-rust (e.g. from a script or GDExtension). The fallback branch can either be `_` (discard object), or
16-
/// `variable @ _` to access the original object inside the fallback arm.
16+
/// `variable` to access the original object inside the fallback arm.
1717
///
1818
/// # Example
1919
/// ```no_run
@@ -26,15 +26,15 @@
2626
/// // Basic syntax.
2727
/// let event: Gd<InputEvent> = some_input();
2828
///
29-
/// let simple_dispatch: i32 = match_class!(event, {
29+
/// let simple_dispatch: i32 = match_class! { event,
3030
/// button @ InputEventMouseButton => 1,
3131
/// motion @ InputEventMouseMotion => 2,
3232
/// action @ InputEventAction => 3,
3333
/// _ => 0, // Fallback.
34-
/// });
34+
/// };
3535
///
3636
/// // More diverse dispatch patterns are also supported.
37-
/// let fancy_dispatch: i32 = match_class!(some_input(), {
37+
/// let fancy_dispatch: i32 = match_class! { some_input(),
3838
/// button @ InputEventMouseButton => 1,
3939
///
4040
/// // Block syntax for multiple statements:
@@ -47,58 +47,54 @@
4747
/// action @ godot::classes::InputEventAction => 3,
4848
///
4949
/// // Fallback with variable -- retrieves original Gd<InputEvent>.
50-
/// // Equivalent to pattern `original @ InputEvent`.
51-
/// original @ _ => 0,
52-
/// });
50+
/// original => 0,
51+
/// };
5352
///
5453
/// // event_type is now 0, 1, 2, or 3
5554
/// ```
5655
///
57-
/// # Limitations
58-
/// The expression block is currently wrapped by a closure, so you cannot use control-flow statements like `?`, `return`, `continue`, `break`.
56+
/// # Expression and control flow
57+
/// The `match_class!` macro is an expression, as such it has a type. If that type is not `()`, you typically need to use the expression or
58+
/// end it with a semicolon.
59+
///
60+
/// Control-flow statements like `?`, `return`, `continue`, `break` can be used within the match arms.
5961
#[macro_export]
6062
// Note: annoyingly shows full implementation in docs. For workarounds, either move impl to a helper macro, or use something like
6163
// https://crates.io/crates/clean-macro-docs.
64+
// Earlier syntax expected curly braces, i.e.: ($subject:expr, { $($tt:tt)* }) => {{ ... }};
6265
macro_rules! match_class {
63-
// TT muncher approach: consume one arm at a time and recurse with the remaining tokens.
66+
($subject:expr, $($tt:tt)*) => {{
67+
let subject = $subject;
68+
$crate::match_class_muncher!(subject, $($tt)*)
69+
}};
70+
}
6471

65-
// Entry point: grab subject + ENTIRE list of arms as token-trees.
66-
($subject:expr, { $($arms:tt)* }) => {
67-
(|| {
68-
let mut __match_subject = $subject;
69-
match_class!(@munch __match_subject; $($arms)*);
70-
// unreachable!("match_class hit end with no `_` fallback");
71-
})()
72-
};
72+
#[doc(hidden)]
73+
#[macro_export]
74+
macro_rules! match_class_muncher {
75+
/*
76+
// If we want to support `var @ _ => { ... }` syntax, use this branch first (to not match `_` as type).
77+
($subject:ident, $var:ident @ _ => $block:expr $(,)?) => {{
78+
let $var = $subject;
79+
$block
80+
}};
81+
*/
7382

74-
// ident @ Some::Path => expr, rest...
75-
(@munch $evt:ident;
76-
$var:ident @ $($class:ident)::+ => $body:expr,
77-
$($rest:tt)*
78-
) => {
79-
// try the down‐cast
80-
$evt = match $evt.try_cast::< $($class)::* >() {
81-
Ok($var) => return $body,
82-
Err(e) => e,
83-
};
84-
match_class!(@munch $evt; $($rest)*);
85-
};
83+
($subject:ident, $var:ident @ $Ty:ty => $block:expr, $($rest:tt)*) => {{
84+
match $subject.try_cast::<$Ty>() {
85+
Ok($var) => $block,
86+
Err(__obj) => {
87+
$crate::match_class_muncher!(__obj, $($rest)*)
88+
}
89+
}
90+
}};
8691

87-
// foo @ _ => expr
88-
(@munch $evt:ident;
89-
$fallback_var:ident @ $pat:tt => $fallback:expr
90-
$(,)?
91-
) => {
92-
// `$pat` here will only ever be `_` if no typed‐arm matched first, because `Some::Path` would hit the more specific rule above.
93-
let $fallback_var = $evt;
94-
return $fallback;
95-
};
92+
($subject:ident, $var:ident => $block:expr $(,)?) => {{
93+
let $var = $subject;
94+
$block
95+
}};
9696

97-
// _ => expr
98-
(@munch $evt:ident;
99-
_ => $fallback:expr
100-
$(,)?
101-
) => {{
102-
return $fallback;
97+
($subject:ident, _ => $block:expr $(,)?) => {{
98+
$block
10399
}};
104100
}

itest/rust/src/engine_tests/match_class_test.rs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ fn match_class_basic_dispatch() {
1919
let obj: Gd<Object> = node2d.upcast();
2020
let to_free = obj.clone();
2121

22-
let result = match_class!(obj, {
22+
let result = match_class! { obj,
2323
node @ Node2D => {
2424
require_node2d(&node);
2525
1
@@ -29,7 +29,7 @@ fn match_class_basic_dispatch() {
2929
2
3030
},
3131
_ => 3 // No comma.
32-
});
32+
};
3333

3434
assert_eq!(result, 1);
3535
to_free.free();
@@ -41,11 +41,11 @@ fn match_class_shadowed_by_more_general() {
4141
let obj: Gd<Object> = node2d.upcast();
4242
let to_free = obj.clone();
4343

44-
let result = match_class!(obj, {
44+
let result = match_class! { obj,
4545
_node @ Node => 1,
4646
_node @ Node2D => 2,
4747
_ => 3, // Comma.
48-
});
48+
};
4949

5050
assert_eq!(
5151
result, 1,
@@ -58,11 +58,11 @@ fn match_class_shadowed_by_more_general() {
5858
fn match_class_ignored_fallback() {
5959
let obj: Gd<Object> = RefCounted::new_gd().upcast();
6060

61-
let result = match_class!(obj, {
61+
let result = match_class! { obj,
6262
_node @ godot::classes::Node => 1, // Test qualified types.
6363
_res @ Resource => 2,
6464
_ => 3,
65-
});
65+
};
6666

6767
assert_eq!(result, 3);
6868
}
@@ -71,29 +71,53 @@ fn match_class_ignored_fallback() {
7171
fn match_class_named_fallback_matched() {
7272
let obj: Gd<Object> = Resource::new_gd().upcast();
7373

74-
let result = match_class!(obj, {
74+
let result = match_class! { obj,
7575
_node @ Node => 1,
7676
_node @ Node2D => 2,
7777

7878
// Named fallback with access to original object.
79-
other @ _ => {
79+
other => {
8080
require_object(&other);
8181
assert_eq!(other.get_class(), "Resource".into());
8282
3
8383
}
84-
});
84+
};
8585

8686
assert_eq!(result, 3);
8787
}
8888

8989
#[itest]
9090
fn match_class_named_fallback_unmatched() {
9191
// Test complex inline expression.
92-
let result = match_class!(Resource::new_gd().upcast::<Object>(), {
92+
let result = match_class! {
93+
Resource::new_gd().upcast::<Object>(),
9394
_node @ Node => 1,
9495
_res @ Resource => 2,
95-
_ignored @ _ => 3,
96-
});
96+
_ignored => 3,
97+
};
9798

9899
assert_eq!(result, 2);
99100
}
101+
102+
#[itest]
103+
fn match_class_control_flow() {
104+
let obj: Gd<Object> = Resource::new_gd().upcast();
105+
106+
let mut broken = false;
107+
108+
#[allow(clippy::never_loop)]
109+
for _i in 0..1 {
110+
match_class! { obj.clone(),
111+
_node @ Node => 1,
112+
_res @ Resource => {
113+
broken = true;
114+
break;
115+
},
116+
_ => 2
117+
};
118+
119+
panic!("break didn't work");
120+
}
121+
122+
assert!(broken, "break statement should have been executed");
123+
}

0 commit comments

Comments
 (0)