rust-quiz

repository·master·Indexed 24 days ago

https://github.com/dtolnay/rust-quiz

A web-based quiz platform featuring medium to hard Rust questions with complete explanations. Inspired by cppquiz.org, it presents users with Rust code snippets and asks them to predict the output, covering advanced topics such as macro_rules! tokenization, opaque metavariables, Higher Ranked Trait Bounds (HRTB), and Zero Sized Types (ZSTs).

Tokens
8.5K
Snippets
12
Records
43
Agent score
83%

What's inside rust-quiz

  1. Understand lazy evaluation in Iterator::map

    master

    In Rust, the Iterator::map method performs its operation lazily. This means the closure provided to map is not executed immediately on the entire input stream. Instead, the closure is only invoked for each element as that element is actually consumed from the resulting iterator (e.g., by a for loop or a method like .collect()).

    Because of this laziness, if you interleave consumption from a mapped iterator with other operations, the closure will be evaluated one step at a time, driving the output sequentially as the iterator is advanced.

  2. Understand trailing commas in 1-tuples

    master

    In Rust, a trailing comma is mandatory for 1-tuples to disambiguate them from parenthesized expressions.

    • (0,) is a 1-tuple containing the value 0.
    • (0) is simply the integer 0 wrapped in parentheses.

    For tuples with two or more elements, the trailing comma is entirely optional. For example, (i32, i32) and (i32, i32,) represent the same type.

  3. Understand infallible matching with unit structs

    master
    In Rust, you can perform an infallible match using a unit struct pattern to destructure a value without binding it to a variable. When you use a pattern like let S = f() where S is a unit struct, you are matching the value of type S but not creating a new owner for it. Because no variable is bound to the returned value, the value is not owned by any local variable and therefore is not dropped at that specific point in the code. This is useful for consuming a value or matching a pattern where you do not need to retain ownership of the contents.
  4. Understand closure parsing differences between bitwise-AND and references

    master

    In Rust, the way a closure body is parsed can change its type based on whether an expression is interpreted as a bitwise-AND operation or a reference.

    If a closure body contains an expression like &S(4) following a statement, it may be parsed as an empty block {} followed by a reference to S(4). This results in a closure type of impl Fn() -> &'static S.

    In contrast, if the expression is parsed as an invocation of a bitwise-AND operator (via the BitAnd trait), the closure returns () (unit type).

  5. Understand the value of assignment expressions in Rust

    master

    In Rust, assignment expressions (e.g., a = true) always evaluate to the unit type (). This means that if you assign the result of an assignment to a new variable, that variable will hold the unit value (), not the value that was assigned.

    For example, the following code results in b being of type ():

    let a = true;
    let b = a = true;
    // b is now ()

    Because () is a Zero-Sized Type (ZST), it occupies zero bytes of memory at runtime.

    let a = true;
    let b = a = true;
    print!("{}", mem::size_of_val(&b)); // Prints 0
  6. How macro_rules! tokenizes punctuation

    master

    In macro_rules! macros, adjacent punctuation characters are grouped into tokens based on how they are defined in the native Rust grammar. The parser uses a greedy process to decompose sequences of characters into these native tokens.

    Key behaviors:

    • Multi-character tokens: Sequences like <<= are treated as a single token because they represent a specific native Rust operator (left shift assignment).
    • Decomposition: If a sequence is not a native token (e.g., =<<), the parser decomposes it into the largest possible native tokens. For example, =<< is decomposed into = and <<.
    • Spacing: Because of this greedy decomposition, writing = << is functionally identical to writing =<< if =<< is not a native token. Similarly, if ==> is not a native token, it decomposes into == and >.

    Note on Procedural Macros: Unlike macro_rules!, procedural macros (using the syn crate) use a more flexible API and can distinguish between different spacings of the same characters (e.g., they can tell the difference between == > and ==>).

  7. Understand method resolution priority for inherent vs trait methods

    master

    In Rust, if a type has both an inherent method and a trait method with the same name and receiver type, the plain method call syntax (e.g., S.f()) will always prefer the inherent method.

    To call the trait method instead, you must use fully qualified syntax:

    • Trait::f(&S)
    • <S as Trait>::f(&S)

    Best Practice for Macro Authors: Avoid using method call syntax to invoke trait methods on user-defined types, as these calls can be unintentionally hijacked by inherent methods with the same name.

  8. Compare Dynamic vs Static Dispatch with Traits

    master

    When calling a trait method, the behavior depends on whether you are using dynamic dispatch (trait objects) or static dispatch (generics):

    Dynamic Dispatch (dyn Trait)

    Uses a trait object (a shim containing function pointers). When you call x.method() on a dyn Base, the compiler looks up the implementation of Base::method for the concrete type via a vtable. It cannot resolve to a method from a different trait, even if that trait is a supertrait of Base.

    Static Dispatch (Generics)

    Uses monomorphization. In a generic function fn foo<T: Base>(x: T), the call x.method() is resolved at compile time to <T as Base>::method. Because type inference for generics happens independently of concrete instantiation, the compiler determines which trait method is being called based on the trait bounds provided, before it even knows the concrete type T.

  9. How match-arm guards interact with OR patterns

    master

    In Rust, when using an if guard on a match arm that contains OR patterns (|), the guard applies to all alternatives within that arm.

    Additionally, if a guard evaluates to false, Rust performs a form of "backtracking": it will attempt to match the subsequent alternatives in the same OR-pattern arm, executing the guard again for each alternative. It does not fail the entire match arm immediately upon the first guard failure.

  10. Understand how Clone behaves for references and Rc<T>

    master

    In Rust, the Clone trait behaves differently depending on whether you are cloning a reference, a zero-sized type, or a smart pointer like Rc<T>:

    • References (&T): Immutable references implement Clone even if the underlying type T does not. Calling .clone() on a reference &T produces another reference &T by duplicating the pointer. This can sometimes trigger unexpectedly when a type does not implement Clone itself.
    • Zero-Sized Types (ZSTs): For types like () (unit type), the compiler prefers the Clone implementation for the type itself (e.g., converting &() to ()) over the Clone implementation for references (e.g., converting &&() to &()) because it requires fewer implicit dereferences.
    • Smart Pointers (Rc<T>): Cloning an Rc<T> increments the reference count rather than cloning the underlying data.

    Best Practice: It is considered idiomatic to use Rc::clone(&c) instead of c.clone() to make it explicitly clear that you are performing a reference count bump rather than a deep clone of the data.

  11. Understand Rust macro hygiene

    master

    Macro hygiene in Rust prevents unintended name collisions by ensuring that identifiers introduced within a macro do not accidentally shadow or interfere with variables in the scope where the macro is called.

    Rather than implementing hygiene by wrapping macro expansions in artificial nested scopes (which would limit their utility), Rust uses a mechanism similar to 'color-coding' identifiers. Each mention of a local variable name is assigned a unique internal identifier (or 'color'). This allows multiple variables with the same textual name to coexist in the same scope without colliding, as the compiler can distinguish between them based on their unique internal identity.

  12. Understand early bound vs late bound lifetime parameters

    master

    In Rust, generic parameters can be either early bound or late bound. This distinction affects how and when the compiler determines the parameter's value.

    Early Bound Parameters

    Early bound parameters are determined by the compiler during monomorphization. Type parameters are always early bound. Because they must be resolved at the time of monomorphization, you cannot have a value whose type has an unresolved type parameter.

    fn m<T>() {}
    
    fn main() {
        let m1 = m::<u8>; // ok
        let m2 = m; // error: cannot infer type for `T`
    }

    Late Bound Parameters

    Lifetime parameters are often late bound, meaning the actual choice of lifetime depends on how the function is called at the call site. Because the lifetime can be different for each call, you cannot specify the lifetime explicitly on the function itself before it is called.

    // error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
    let m2 = m::<'static>;
    
    // error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
    let m3 = m::<'_>;

    Higher Ranked Trait Bounds (HRTB)

    // error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
    let m2 = m::<'static>;
    
    // error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
    let m3 = m::<'_];