diff --git a/contrib/rst_files_with_prelude.txt b/contrib/rst_files_with_prelude.txt index ac527f9ea..519ec3958 100644 --- a/contrib/rst_files_with_prelude.txt +++ b/contrib/rst_files_with_prelude.txt @@ -10,3 +10,4 @@ courses/gnattest/*.rst courses/gnat_project_facility/*.rst courses/gnatcoverage/*.rst courses/rust_essentials/*.rst +courses/comprehensive_rust_training/*.rst diff --git a/courses/advanced_exception_analysis/010_advanced_exception_analysis.rst b/courses/advanced_exception_analysis/010_advanced_exception_analysis.rst index 9e322ed91..3f1110947 100644 --- a/courses/advanced_exception_analysis/010_advanced_exception_analysis.rst +++ b/courses/advanced_exception_analysis/010_advanced_exception_analysis.rst @@ -15,6 +15,9 @@ Advanced Exception Analysis .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/comprehensive_rust_training/010_introduction.rst b/courses/comprehensive_rust_training/010_introduction.rst new file mode 100644 index 000000000..871d7e9d0 --- /dev/null +++ b/courses/comprehensive_rust_training/010_introduction.rst @@ -0,0 +1,44 @@ +************ +Introduction +************ + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. + About Adacore + About This Training + +.. container:: PRELUDE END + +.. include:: 010_introduction/01-about_adacore.rst +.. include:: 010_introduction/02-about_this_training.rst diff --git a/courses/comprehensive_rust_training/010_introduction/01-about_adacore.rst b/courses/comprehensive_rust_training/010_introduction/01-about_adacore.rst new file mode 100644 index 000000000..24d52ae24 --- /dev/null +++ b/courses/comprehensive_rust_training/010_introduction/01-about_adacore.rst @@ -0,0 +1,21 @@ +============= +About AdaCore +============= + +----------- +The Company +----------- + +.. + Taken from https://www.adacore.com/company/about + +* Founded in 1994 +* Centered around helping developers build **safe, secure and reliable** software +* Headquartered in New York and Paris + + - Representatives in countries around the globe + +* Roots in Open Source software movement + + - GNAT compiler is part of GNU Compiler Collection (GCC) + diff --git a/courses/comprehensive_rust_training/010_introduction/02-about_this_training.rst b/courses/comprehensive_rust_training/010_introduction/02-about_this_training.rst new file mode 100644 index 000000000..81b578774 --- /dev/null +++ b/courses/comprehensive_rust_training/010_introduction/02-about_this_training.rst @@ -0,0 +1,83 @@ +=================== +About This Training +=================== + +-------------------------- +Your Trainer +-------------------------- + +* Experience in software development + + - Languages + - Methodology + +* Experience teaching this class + +----------------------------- +Goals of the training session +----------------------------- + +* What you should know by the end of the training +* Syllabus overview + + - The syllabus is a guide, but we might stray off of it + - ...and that's OK: we're here to cover **your needs** + +---------- +Roundtable +---------- + +* 5 minute exercise + + - Write down your answers to the following + - Then share it with the room + +* Experience in software development + + - Languages + - Methodology + +* Experience and interest with the syllabus + + - Current and upcoming projects + - Curious for something? + +* Your personal goals for this training + + - What do you want to have coming out of this? + +* Anecdotes, stories... feel free to share! + + - Most interesting or funny bug you've encountered? + - Your own programming interests? + +------------------- +Course Presentation +------------------- + +* Slides +* Quizzes +* Labs + + - Hands-on practice + - Recommended setup: latest GNAT Studio + - Class reflection after some labs + +* Demos + + - Depending on the context + +* Daily schedule + +-------- +Styles +-------- + +* :dfn:`This` is a definition +* :filename:`this/is/a.path` +* :ada:`code is highlighted` +* :command:`commands are emphasised --like-this` + +.. warning:: This is a warning +.. note:: This is an important piece of info +.. tip:: This is a tip diff --git a/courses/comprehensive_rust_training/020_hello_world.rst b/courses/comprehensive_rust_training/020_hello_world.rst new file mode 100644 index 000000000..dfebc51e3 --- /dev/null +++ b/courses/comprehensive_rust_training/020_hello_world.rst @@ -0,0 +1,41 @@ +************* +Hello World +************* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 020_hello_world/01_what_is_rust.rst +.. include:: 020_hello_world/02_benefits.rst +.. include:: 020_hello_world/03_playground.rst diff --git a/courses/comprehensive_rust_training/020_hello_world/01_what_is_rust.rst b/courses/comprehensive_rust_training/020_hello_world/01_what_is_rust.rst new file mode 100644 index 000000000..02a98a5ae --- /dev/null +++ b/courses/comprehensive_rust_training/020_hello_world/01_what_is_rust.rst @@ -0,0 +1,46 @@ +=============== +What is Rust? +=============== + +--------------- +What is Rust? +--------------- + +Rust is a new programming language which had its `1.0 release in +2015 `__: + +- Rust is a statically compiled language in a similar role as C++ + + - :command:`rustc` uses LLVM as its backend. + +- Rust supports many `platforms and + architectures `__: + + - x86, ARM, WebAssembly, ... + - Linux, Mac, Windows, ... + +- Rust is used for a wide range of devices: + + - firmware and boot loaders, + - smart displays, + - mobile phones, + - desktops, + - servers. + +.. raw:: html + +--------- +Details +--------- + +Rust fits in the same area as C++: + +- High flexibility. +- High level of control. +- Can be scaled down to very constrained devices such as + microcontrollers. +- Has no runtime or garbage collection. +- Focuses on reliability and safety without sacrificing performance. + +.. raw:: html + diff --git a/courses/comprehensive_rust_training/020_hello_world/02_benefits.rst b/courses/comprehensive_rust_training/020_hello_world/02_benefits.rst new file mode 100644 index 000000000..519d21d88 --- /dev/null +++ b/courses/comprehensive_rust_training/020_hello_world/02_benefits.rst @@ -0,0 +1,65 @@ +================== +Benefits of Rust +================== + +------------------ +Benefits of Rust +------------------ + +Some unique selling points of Rust: + +- *Compile time memory safety* - whole classes of memory bugs are + prevented at compile time + + - No uninitialized variables. + - No double-frees. + - No use-after-free. + - No null pointers. + - No forgotten locked mutexes. + - No data races between threads. + - No iterator invalidation. + +- *No undefined runtime behavior* - what a Rust statement does is never + left unspecified + + - Array access is bounds checked. + - Integer overflow is defined (panic or wrap-around). + +- *Modern language features* - as expressive and ergonomic as + higher-level languages + + - Enums and pattern matching. + - Generics. + - No overhead FFI. + - Zero-cost abstractions. + - Great compiler errors. + - Built-in dependency manager. + - Built-in support for testing. + - Excellent Language Server Protocol support. + +.. raw:: html + +--------- +Details +--------- + +Do not spend much time here. All of these points will be covered in more +depth later. + +Make sure to ask the class which languages they have experience with. +Depending on the answer you can highlight different features of Rust: + +- Experience with C or C++: Rust eliminates a whole class of *runtime + errors* via the borrow checker. You get performance like in C and + C++, but you don't have the memory unsafety issues. In addition, you + get a modern language with constructs like pattern matching and + built-in dependency management. + +- Experience with Java, Go, Python, JavaScript...: You get the same + memory safety as in those languages, plus a similar high-level + language feeling. In addition you get fast and predictable + performance like C and C++ (no garbage collector) as well as access + to low-level hardware (should you need it). + +.. raw:: html + diff --git a/courses/comprehensive_rust_training/020_hello_world/03_playground.rst b/courses/comprehensive_rust_training/020_hello_world/03_playground.rst new file mode 100644 index 000000000..4bacb2fa9 --- /dev/null +++ b/courses/comprehensive_rust_training/020_hello_world/03_playground.rst @@ -0,0 +1,37 @@ +============ +Playground +============ + +------------ +Playground +------------ + +The `Rust Playground `__ provides an easy +way to run short Rust programs, and is the basis for the examples and +exercises in this course. Try running the "hello-world" program it +starts with. It comes with a few handy features: + +- Under "Tools", use the :menu:`rustfmt` option to format your code in the + "standard" way. + +- Rust has two main "profiles" for generating code: Debug (extra + runtime checks, less optimization) and Release (fewer runtime checks, + lots of optimization). These are accessible under "Debug" at the top. + +- If you're interested, use :menu:`ASM` under :menu:`...` to see the generated + assembly code. + +.. raw:: html + +--------- +Details +--------- + +As students head into the break, encourage them to open up the +playground and experiment a little. Encourage them to keep the tab open +and try things out during the rest of the course. This is particularly +helpful for advanced students who want to know more about Rust's +optimizations or generated assembly. + +.. raw:: html + diff --git a/courses/comprehensive_rust_training/030_types_and_values.rst b/courses/comprehensive_rust_training/030_types_and_values.rst new file mode 100644 index 000000000..4444b8c0b --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values.rst @@ -0,0 +1,44 @@ +****************** +Types And Values +****************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 030_types_and_values/01_hello_world.rst +.. include:: 030_types_and_values/02_variables.rst +.. include:: 030_types_and_values/03_values.rst +.. include:: 030_types_and_values/04_arithmetic.rst +.. include:: 030_types_and_values/05_inference.rst +.. include:: 030_types_and_values/06_exercise.rst diff --git a/courses/comprehensive_rust_training/030_types_and_values/01_hello_world.rst b/courses/comprehensive_rust_training/030_types_and_values/01_hello_world.rst new file mode 100644 index 000000000..6661e6cc8 --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/01_hello_world.rst @@ -0,0 +1,56 @@ +============== +Hello, World +============== + +-------------- +Hello, World +-------------- + +Let us jump into the simplest possible Rust program, a classic Hello +World program: + +.. code:: rust + + fn main() { + println!("Hello World!"); + } + +What you see: + +- Functions are introduced with :rust:`fn`. +- Blocks are delimited by curly braces like in C and C++. +- The :rust:`main` function is the entry point of the program. +- Rust has hygienic macros, :rust:`println!` is an example of this. +- Rust strings are UTF-8 encoded and can contain any Unicode character. + +--------- +Details +--------- + +This slide tries to make the students comfortable with Rust code. They +will see a ton of it over the next four days so we start small with +something familiar. + +Key points: + +- Rust is very much like other languages in the C/C++/Java tradition. + It is imperative and it doesn't try to reinvent things unless + absolutely necessary. + +- Rust is modern with full support for things like Unicode. + +- Rust uses macros for situations where you want to have a variable + number of arguments (no function + `overloading <../control-flow-basics/functions.md>`__). + +- Macros being 'hygienic' means they don't accidentally capture + identifiers from the scope they are used in. Rust macros are actually + only `partially + hygienic `__. + +- Rust is multi-paradigm. For example, it has powerful `object-oriented + programming + features `__, and, + while it is not a functional language, it includes a range of + `functional + concepts `__. diff --git a/courses/comprehensive_rust_training/030_types_and_values/02_variables.rst b/courses/comprehensive_rust_training/030_types_and_values/02_variables.rst new file mode 100644 index 000000000..7281e9911 --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/02_variables.rst @@ -0,0 +1,36 @@ +=========== +Variables +=========== + +----------- +Variables +----------- + +Rust provides type safety via static typing. Variable bindings are made +with :rust:`let`: + +.. code:: rust + + fn main() { + let x: i32 = 10; + println!("x: {x}"); + // x = 20; + // println!("x: {x}"); + } + +--------- +Details +--------- + +- Uncomment the :rust:`x = 20` to demonstrate that variables are immutable + by default. Add the :rust:`mut` keyword to allow changes. + +- Warnings are enabled for this slide, such as for unused variables or + unnecessary :rust:`mut`. These are omitted in most slides to avoid + distracting warnings. Try removing the mutation but leaving the + :rust:`mut` keyword in place. + +- The :rust:`i32` here is the type of the variable. This must be known at + compile time, but type inference (covered later) allows the + programmer to omit it in many cases. + diff --git a/courses/comprehensive_rust_training/030_types_and_values/03_values.rst b/courses/comprehensive_rust_training/030_types_and_values/03_values.rst new file mode 100644 index 000000000..1a865d0ac --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/03_values.rst @@ -0,0 +1,48 @@ +======== +Values +======== + +-------- +Values +-------- + +Here are some basic built-in types, and the syntax for literal values of +each type. + ++---------------+-------------------------------+---------------------+ +| | Types | Literals | ++===============+===============================+=====================+ +| Signed | ``i8``, ``i16``, ``i32``, | ``-10``, ``0``, | +| integers | ``i64``, ``i128``, ``isize`` | ``1_000``, | +| | | ``123_i64`` | ++---------------+-------------------------------+---------------------+ +| Unsigned | ``u8``, ``u16``, ``u32``, | ``0``, ``123``, | +| integers | ``u64``, ``u128``, ``usize`` | ``10_u16`` | ++---------------+-------------------------------+---------------------+ +| Floating | ``f32``, ``f64`` | ``3.14``, | +| point numbers | | ``-10.0e20``, | +| | | ``2_f32`` | ++---------------+-------------------------------+---------------------+ +| Unicode | ``char`` | ``'a'``, | +| scalar values | | ':math:`\alpha`', | +| | | ':math:`\infty`' | ++---------------+-------------------------------+---------------------+ +| Booleans | ``bool`` | ``true``, ``false`` | ++---------------+-------------------------------+---------------------+ + +The types have widths as follows: + +- :rust:`iN`, :rust:`uN`, and :rust:`fN` are *N* bits wide, +- :rust:`isize` and :rust:`usize` are the width of a pointer, +- :rust:`char` is 32 bits wide, +- :rust:`bool` is 8 bits wide. + +--------- +Details +--------- + +There are a few syntaxes which are not shown above: + +- All underscores in numbers can be left out, they are for legibility + only. So :rust:`1_000` can be written as :rust:`1000` (or :rust:`10_00`), and + :rust:`123_i64` can be written as :rust:`123i64`. diff --git a/courses/comprehensive_rust_training/030_types_and_values/04_arithmetic.rst b/courses/comprehensive_rust_training/030_types_and_values/04_arithmetic.rst new file mode 100644 index 000000000..f929b2e1c --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/04_arithmetic.rst @@ -0,0 +1,40 @@ +============ +Arithmetic +============ + +------------ +Arithmetic +------------ + +.. code:: rust + + fn interproduct(a: i32, b: i32, c: i32) -> i32 { + return a * b + b * c + c * a; + } + + fn main() { + println!("result: {}", interproduct(120, 100, 248)); + } + +--------- +Details +--------- + +This is the first time we've seen a function other than :rust:`main`, but +the meaning should be clear: it takes three integers, and returns an +integer. Functions will be covered in more detail later. + +Arithmetic is very similar to other languages, with similar precedence. + +What about integer overflow? In C and C++ overflow of *signed* integers +is actually undefined, and might do unknown things at runtime. In Rust, +it's defined. + +Change the :rust:`i32` to :rust:`i16` to see an integer overflow, which +panics (checked) in a debug build and wraps in a release build. There +are other options, such as overflowing, saturating, and carrying. These +are accessed with method syntax, e.g., +:rust:`(a * b).saturating_add(b * c).saturating_add(c * a)`. + +In fact, the compiler will detect overflow of constant expressions, +which is why the example requires a separate function. diff --git a/courses/comprehensive_rust_training/030_types_and_values/05_inference.rst b/courses/comprehensive_rust_training/030_types_and_values/05_inference.rst new file mode 100644 index 000000000..c555187b1 --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/05_inference.rst @@ -0,0 +1,54 @@ +================ +Type Inference +================ + +---------------- +Type Inference +---------------- + +Rust will look at how the variable is *used* to determine the type: + +.. code:: rust + + fn takes_u32(x: u32) { + println!("u32: {x}"); + } + + fn takes_i8(y: i8) { + println!("i8: {y}"); + } + + fn main() { + let x = 10; + let y = 20; + + takes_u32(x); + takes_i8(y); + // takes_u32(y); + } + +--------- +Details +--------- + +This slide demonstrates how the Rust compiler infers types based on +constraints given by variable declarations and usages. + +It is very important to emphasize that variables declared like this are +not of some sort of dynamic "any type" that can hold any data. The +machine code generated by such declaration is identical to the explicit +declaration of a type. The compiler does the job for us and helps us +write more concise code. + +When nothing constrains the type of an integer literal, Rust defaults to +:rust:`i32`. This sometimes appears as :rust:`{integer}` in error messages. +Similarly, floating-point literals default to :rust:`f64`. + +.. code:: rust + + fn main() { + let x = 3.14; + let y = 20; + assert_eq!(x, y); + // ERROR: no implementation for `{float} == {integer}` + } diff --git a/courses/comprehensive_rust_training/030_types_and_values/06_exercise.rst b/courses/comprehensive_rust_training/030_types_and_values/06_exercise.rst new file mode 100644 index 000000000..64669332e --- /dev/null +++ b/courses/comprehensive_rust_training/030_types_and_values/06_exercise.rst @@ -0,0 +1,28 @@ +===================== +Exercise: Fibonacci +===================== + +--------------------- +Exercise: Fibonacci +--------------------- + +The Fibonacci sequence begins with :rust:`[0,1]`. For n>1, the n'th +Fibonacci number is calculated recursively as the sum of the n-1'th and +n-2'th Fibonacci numbers. + +Write a function :rust:`fib(n)` that calculates the n'th Fibonacci number. +When will this function panic? + +.. code:: rust + + {{#include exercise.rs:fib}} + if n < 2 { + // The base case. + return todo!("Implement this"); + } else { + // The recursive case. + return todo!("Implement this"); + } + } + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/040_control_flow_basics.rst b/courses/comprehensive_rust_training/040_control_flow_basics.rst new file mode 100644 index 000000000..856f38bac --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics.rst @@ -0,0 +1,46 @@ +********************* +Control Flow Basics +********************* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 040_control_flow_basics/01_if.rst +.. include:: 040_control_flow_basics/02_match.rst +.. include:: 040_control_flow_basics/03_loops.rst +.. include:: 040_control_flow_basics/04_break_continue.rst +.. include:: 040_control_flow_basics/05_blocks_and_scopes.rst +.. include:: 040_control_flow_basics/06_functions.rst +.. include:: 040_control_flow_basics/07_macros.rst +.. include:: 040_control_flow_basics/08_exercise.rst diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/01_if.rst b/courses/comprehensive_rust_training/040_control_flow_basics/01_if.rst new file mode 100644 index 000000000..1d5f09728 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/01_if.rst @@ -0,0 +1,49 @@ +==================== +"if" expressions +==================== + +-------------------- +"if" expressions +-------------------- + +You use +`"if expressions" `__ +exactly like :rust:`if` statements in other languages: + +.. code:: rust + + fn main() { + let x = 10; + if x == 0 { + println!("zero!"); + } else if x < 100 { + println!("biggish"); + } else { + println!("huge"); + } + } + +In addition, you can use :rust:`if` as an expression. The last expression of +each block becomes the value of the :rust:`if` expression: + +.. code:: rust + + fn main() { + let x = 10; + let size = if x < 20 { "small" } else { "large" }; + println!("number size: {}", size); + } + + +--------- +Details +--------- + +Because :rust:`if` is an expression and must have a particular type, both of +its branch blocks must have the same type. Show what happens if you add +:rust:`;` after :rust:`"small"` in the second example. + +An :rust:`if` expression should be used in the same way as the other +expressions. For example, when it is used in a :rust:`let` statement, the +statement must be terminated with a :rust:`;` as well. Remove the :rust:`;` +before :rust:`println!` to see the compiler error. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/02_match.rst b/courses/comprehensive_rust_training/040_control_flow_basics/02_match.rst new file mode 100644 index 000000000..2846f0a82 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/02_match.rst @@ -0,0 +1,80 @@ +======================= +"match" Expressions +======================= + +----------------------- +"match" Expressions +----------------------- + +:rust:`match` can be used to check a value against one or more options: + +.. code:: rust + + fn main() { + let val = 1; + match val { + 1 => println!("one"), + 10 => println!("ten"), + 100 => println!("one hundred"), + _ => { + println!("something else"); + } + } + } + +Like :rust:`if` expressions, :rust:`match` can also return a value; + +.. code:: rust + + fn main() { + let flag = true; + let val = match flag { + true => 1, + false => 0, + }; + println!("The value of {flag} is {val}"); + } + +--------- +Details +--------- + +- :rust:`match` arms are evaluated from top to bottom, and the first one + that matches has its corresponding body executed. + +- There is no fall-through between cases the way that :rust:`switch` works + in other languages. + +- The body of a :rust:`match` arm can be a single expression or a block. + Technically this is the same thing, since blocks are also + expressions, but students may not fully understand that symmetry at + this point. + +- :rust:`match` expressions need to be exhaustive, meaning they either need + to cover all possible values or they need to have a default case such + as :rust:`_`. Exhaustiveness is easiest to demonstrate with enums, but + enums haven't been introduced yet. Instead we demonstrate matching on + a :rust:`bool`, which is the simplest primitive type. + +- This slide introduces :rust:`match` without talking about pattern + matching, giving students a chance to get familiar with the syntax + without front-loading too much information. We'll be talking about + pattern matching in more detail tomorrow, so try not to go into too + much detail here. + +----------------- +More to Explore +----------------- + +- To further motivate the usage of :rust:`match`, you can compare the + examples to their equivalents written with :rust:`if`. In the second case + matching on a :rust:`bool` an :rust:`if {} else {}` block is pretty similar. + But in the first example that checks multiple cases, a :rust:`match` + expression can be more concise than + :rust:`if {} else if {} else if {} else`. + +- :rust:`match` also supports match guards, which allow you to add an + arbitrary logical condition that will get evaluated to determine if + the match arm should be taken. However talking about match guards + requires explaining about pattern matching, which we're trying to + avoid on this slide. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/03_loops.rst b/courses/comprehensive_rust_training/040_control_flow_basics/03_loops.rst new file mode 100644 index 000000000..1fe8eec16 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/03_loops.rst @@ -0,0 +1,86 @@ +======= +Loops +======= + +------- +Loops +------- + +There are three looping keywords in Rust: :rust:`while`, :rust:`loop`, and +:rust:`for`: + +----------- +"while" +----------- + +The +`while keyword `__ +works much like in other languages, executing the loop body as long as +the condition is true. + +.. code:: rust + + fn main() { + let mut x = 200; + while x >= 10 { + x = x / 2; + } + println!("Final x: {x}"); + } + +------- +"for" +------- + +The `for loop `__ iterates over +ranges of values or the items in a collection: + +.. code:: rust + + fn main() { + for x in 1..5 { + println!("x: {x}"); + } + + for elem in [1, 2, 3, 4, 5] { + println!("elem: {elem}"); + } + } + +--------------- +"for" Details +--------------- + +- Under the hood :rust:`for` loops use a concept called :dfn:`iterators` to handle + iterating over different kinds of ranges/collections. Iterators will be + discussed in more detail later. +- Note that the first :rust:`for` loop only iterates to :rust:`4`. Show the :rust:`1..=5` syntax + for an inclusive range. + +-------- +"loop" +-------- + + +The `loop statement `__ just +loops forever, until a :rust:`break`. + +.. code:: rust + + fn main() { + let mut i = 0; + loop { + i += 1; + println!("{i}"); + if i > 100 { + break; + } + } + } + +---------------- +"loop" Details +---------------- + +- The :rust:`loop` statement works like a :rust:`while true` loop. Use it for things like + servers which will serve connections forever. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/04_break_continue.rst b/courses/comprehensive_rust_training/040_control_flow_basics/04_break_continue.rst new file mode 100644 index 000000000..a472d68ad --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/04_break_continue.rst @@ -0,0 +1,77 @@ +============================ +"break" and "continue" +============================ + +---------------------------- +"break" and "continue" +---------------------------- + +If you want to immediately start the next iteration use +`continue `__. + +If you want to exit any kind of loop early, use +`break `__. +With :rust:`loop`, this can take an optional expression that becomes the +value of the :rust:`loop` expression. + +.. code:: rust + + fn main() { + let mut i = 0; + loop { + i += 1; + if i > 5 { + break; + } + if i % 2 == 0 { + continue; + } + println!("{}", i); + } + } + +--------- +Details +--------- + +Note that :rust:`loop` is the only looping construct which can return a +non-trivial value. This is because it's guaranteed to only return at a +:rust:`break` statement (unlike :rust:`while` and :rust:`for` loops, which can also +return when the condition fails). + +-------- +Labels +-------- + +Both :rust:`continue` and :rust:`break` can optionally take a label argument which is used +to break out of nested loops: + +.. code:: rust + + fn main() { + let s = [[5, 6, 7], [8, 9, 10], [21, 15, 32]]; + let mut elements_searched = 0; + let target_value = 10; + 'outer: for i in 0..=2 { + for j in 0..=2 { + elements_searched += 1; + if s[i][j] == target_value { + break 'outer; + } + } + } + print!("elements searched: {elements_searched}"); + } + +---------------- +Labels Details +---------------- + +- Labeled break also works on arbitrary blocks, e.g. + +.. code:: rust + + 'label: { + break 'label; + println!("This line gets skipped"); + } diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/05_blocks_and_scopes.rst b/courses/comprehensive_rust_training/040_control_flow_basics/05_blocks_and_scopes.rst new file mode 100644 index 000000000..4532a22e1 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/05_blocks_and_scopes.rst @@ -0,0 +1,72 @@ +=================== +Blocks and Scopes +=================== + +-------- +Blocks +-------- + +A block in Rust contains a sequence of expressions, enclosed by braces +:rust:`{}`. Each block has a value and a type, which are those of the last +expression of the block: + +.. code:: rust + + fn main() { + let z = 13; + let x = { + let y = 10; + println!("y: {y}"); + z - y + }; + println!("x: {x}"); + } + +If the last expression ends with :rust:`;`, then the resulting value and +type is :rust:`()`. + +---------------- +Blocks Details +---------------- + +- You can show how the value of the block changes by changing the last + line in the block. For instance, adding/removing a semicolon or using + a :rust:`return`. + +---------------------- +Scopes and Shadowing +---------------------- + +A variable's scope is limited to the enclosing block. + +You can shadow variables, both those from outer scopes and variables from the +same scope: + +.. code:: rust + + fn main() { + let a = 10; + println!("before: {a}"); + { + let a = "hello"; + println!("inner scope: {a}"); + + let a = true; + println!("shadowed in inner scope: {a}"); + } + + println!("after: {a}"); + } + +------------------------------ +Scopes and Shadowing Details +------------------------------ + +- Show that a variable's scope is limited by adding a :rust:`b` in the inner block in + the last example, and then trying to access it outside that block. +- Shadowing is different from mutation, because after shadowing both variables' + memory locations exist at the same time. Both are available under the same + name, depending where you use it in the code. +- A shadowing variable can have a different type. +- Shadowing looks obscure at first, but is convenient for holding on to values + after :rust:`.unwrap()`. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/06_functions.rst b/courses/comprehensive_rust_training/040_control_flow_basics/06_functions.rst new file mode 100644 index 000000000..415c39872 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/06_functions.rst @@ -0,0 +1,42 @@ +=========== +Functions +=========== + +----------- +Functions +----------- + +.. code:: rust + + fn gcd(a: u32, b: u32) -> u32 { + if b > 0 { + gcd(b, a % b) + } else { + a + } + } + + fn main() { + println!("gcd: {}", gcd(143, 52)); + } + +--------- +Details +--------- + +- Declaration parameters are followed by a type (the reverse of some + programming languages), then a return type. +- The last expression in a function body (or any block) becomes the + return value. Simply omit the :rust:`;` at the end of the expression. The + :rust:`return` keyword can be used for early return, but the "bare value" + form is idiomatic at the end of a function (refactor :rust:`gcd` to use a + :rust:`return`). +- Some functions have no return value, and return the *unit type*, + :rust:`()`. The compiler will infer this if the return type is omitted. +- Overloading is not supported - each function has a single + implementation. + + - Always takes a fixed number of parameters. Default arguments are + not supported. Macros can be used to support variadic functions. + - Always takes a single set of parameter types. These types can be + generic, which will be covered later. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/07_macros.rst b/courses/comprehensive_rust_training/040_control_flow_basics/07_macros.rst new file mode 100644 index 000000000..7cb3bf031 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/07_macros.rst @@ -0,0 +1,52 @@ +======== +Macros +======== + +-------- +Macros +-------- + +Macros are expanded into Rust code during compilation, and can take a +variable number of arguments. They are distinguished by a :rust:`!` at the +end. The Rust standard library includes an assortment of useful macros. + +- :rust:`println!(format, ..)` prints a line to standard output, applying + formatting described in + `std::fmt `__. +- :rust:`format!(format, ..)` works just like :rust:`println!` but returns the + result as a string. +- :rust:`dbg!(expression)` logs the value of the expression and returns it. +- :rust:`todo!()` marks a bit of code as not-yet-implemented. If executed, + it will panic. +- :rust:`unreachable!()` marks a bit of code as unreachable. If executed, + it will panic. + +.. code:: rust + + fn factorial(n: u32) -> u32 { + let mut product = 1; + for i in 1..=n { + product *= dbg!(i); + } + product + } + + fn fizzbuzz(n: u32) -> u32 { + todo!() + } + + fn main() { + let n = 4; + println!("{n}! = {}", factorial(n)); + } + +--------- +Details +--------- + +The takeaway from this section is that these common conveniences exist, +and how to use them. Why they are defined as macros, and what they +expand to, is not especially critical. + +The course does not cover defining macros, but a later section will +describe use of derive macros. diff --git a/courses/comprehensive_rust_training/040_control_flow_basics/08_exercise.rst b/courses/comprehensive_rust_training/040_control_flow_basics/08_exercise.rst new file mode 100644 index 000000000..2825f2e69 --- /dev/null +++ b/courses/comprehensive_rust_training/040_control_flow_basics/08_exercise.rst @@ -0,0 +1,39 @@ +============================ +Exercise: Collatz Sequence +============================ + +---------------------------- +Exercise: Collatz Sequence +---------------------------- + +The +`Collatz Sequence `__ is +defined as follows, for an arbitrary n1 greater than zero: + +- If *ni* is 1, then the sequence terminates at *ni*. +- If *ni* is even, then *ni+1 = ni / 2*. +- If *ni* is odd, then *ni+1 = 3 \* ni + 1*. + +For example, beginning with *n1* = 3: + +- 3 is odd, so *n2* = 3 \* 3 + 1 = 10; +- 10 is even, so *n3* = 10 / 2 = 5; +- 5 is odd, so *n4* = 3 \* 5 + 1 = 16; +- 16 is even, so *n5* = 16 / 2 = 8; +- 8 is even, so *n6* = 8 / 2 = 4; +- 4 is even, so *n7* = 4 / 2 = 2; +- 2 is even, so *n8* = 1; and +- the sequence terminates. + +Write a function to calculate the length of the collatz sequence for a +given initial :rust:`n`. + +.. code:: rust + + {{#include exercise.rs:collatz_length}} + todo!("Implement this") + } + + {{#include exercise.rs:tests}} + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays.rst new file mode 100644 index 000000000..526591d33 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays.rst @@ -0,0 +1,43 @@ +******************* +Tuples And Arrays +******************* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 050_tuples_and_arrays/01_arrays.rst +.. include:: 050_tuples_and_arrays/02_tuples.rst +.. include:: 050_tuples_and_arrays/03_iteration.rst +.. include:: 050_tuples_and_arrays/04_destructuring.rst +.. include:: 050_tuples_and_arrays/05_exercise.rst diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays/01_arrays.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays/01_arrays.rst new file mode 100644 index 000000000..8103f0766 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays/01_arrays.rst @@ -0,0 +1,40 @@ +======== +Arrays +======== + +-------- +Arrays +-------- + +.. code:: rust + + fn main() { + let mut a: [i8; 10] = [42; 10]; + a[5] = 0; + println!("a: {a:?}"); + } + +--------- +Details +--------- + +- A value of the array type :rust:`[T; N]` holds :rust:`N` (a compile-time + constant) elements of the same type :rust:`T`. Note that the length of + the array is *part of its type*, which means that :rust:`[u8; 3]` and + :rust:`[u8; 4]` are considered two different types. Slices, which have a + size determined at runtime, are covered later. + +- Try accessing an out-of-bounds array element. Array accesses are + checked at runtime. Rust can usually optimize these checks away, and + they can be avoided using unsafe Rust. + +- We can use literals to assign values to arrays. + +- The :rust:`println!` macro asks for the debug implementation with the + :rust:`?` format parameter: :rust:`{}` gives the default output, :rust:`{:?}` + gives the debug output. Types such as integers and strings implement + the default output, but arrays only implement the debug output. This + means that we must use debug output here. + +- Adding :rust:`#`, eg :rust:`{a:#?}`, invokes a "pretty printing" format, + which can be easier to read. diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays/02_tuples.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays/02_tuples.rst new file mode 100644 index 000000000..9ae4d05c6 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays/02_tuples.rst @@ -0,0 +1,30 @@ +======== +Tuples +======== + +-------- +Tuples +-------- + +.. code:: rust + + fn main() { + let t: (i8, bool) = (7, true); + println!("t.0: {}", t.0); + println!("t.1: {}", t.1); + } + +--------- +Details +--------- + +- Like arrays, tuples have a fixed length. + +- Tuples group together values of different types into a compound type. + +- Fields of a tuple can be accessed by the period and the index of the + value, e.g. :rust:`t.0`, :rust:`t.1`. + +- The empty tuple :rust:`()` is referred to as the :dfn:`unit type` and + signifies absence of a return value, akin to :rust:`void` in other + languages. diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays/03_iteration.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays/03_iteration.rst new file mode 100644 index 000000000..740ba9c71 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays/03_iteration.rst @@ -0,0 +1,31 @@ +================= +Array Iteration +================= + +----------------- +Array Iteration +----------------- + +The :rust:`for` statement supports iterating over arrays (but not tuples). + +.. code:: rust + + fn main() { + let primes = [2, 3, 5, 7, 11, 13, 17, 19]; + for prime in primes { + for i in 2..prime { + assert_ne!(prime % i, 0); + } + } + } + +--------- +Details +--------- + +This functionality uses the :rust:`IntoIterator` trait, but we haven't +covered that yet. + +The :rust:`assert_ne!` macro is new here. There are also :rust:`assert_eq!` and +:rust:`assert!` macros. These are always checked, while debug-only variants +like :rust:`debug_assert!` compile to nothing in release builds. diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays/04_destructuring.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays/04_destructuring.rst new file mode 100644 index 000000000..91953db98 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays/04_destructuring.rst @@ -0,0 +1,45 @@ +============================ +Patterns and Destructuring +============================ + +---------------------------- +Patterns and Destructuring +---------------------------- + +When working with tuples and other structured values it's common to want +to extract the inner values into local variables. This can be done +manually by directly accessing the inner values: + +.. code:: rust + + fn print_tuple(tuple: (i32, i32)) { + let left = tuple.0; + let right = tuple.1; + println!("left: {left}, right: {right}"); + } + +However, Rust also supports using pattern matching to destructure a +larger value into its constituent parts: + +.. code:: rust + + fn print_tuple(tuple: (i32, i32)) { + let (left, right) = tuple; + println!("left: {left}, right: {right}"); + } + +--------- +Details +--------- + +- The patterns used here are :dfn:`irrefutable`, meaning that the compiler + can statically verify that the value on the right of :rust:`=` has the + same structure as the pattern. +- A variable name is an *irrefutable* pattern that always matches any + value, hence why we can also use :rust:`let` to declare a single + variable. +- Rust also supports using patterns in conditionals, allowing for + equality comparison and destructuring to happen at the same time. + This form of pattern matching will be discussed in more detail later. +- Edit the examples above to show the compiler error when the pattern + doesn't match the value being matched on. diff --git a/courses/comprehensive_rust_training/050_tuples_and_arrays/05_exercise.rst b/courses/comprehensive_rust_training/050_tuples_and_arrays/05_exercise.rst new file mode 100644 index 000000000..8db643227 --- /dev/null +++ b/courses/comprehensive_rust_training/050_tuples_and_arrays/05_exercise.rst @@ -0,0 +1,54 @@ +========================= +Exercise: Nested Arrays +========================= + +------------------------- +Exercise: Nested Arrays +------------------------- + +Arrays can contain other arrays: + +.. code:: rust + + let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; + +What is the type of this variable? + +Use an array such as the above to write a function :rust:`transpose` which +will transpose a matrix (turn rows into columns): + +Transpose + +.. math:: + + \begin{bmatrix} + 1 & 2 & 3 \\ + 4 & 5 & 6 \\ + 7 & 8 & 9 + \end{bmatrix} + +into + +.. math:: + + \begin{bmatrix} + 1 & 4 & 7 \\ + 2 & 5 & 8 \\ + 3 & 6 & 9 + \end{bmatrix} + +Copy the code below to https://play.rust-lang.org/ and implement the +function. This function only operates on 3x3 matrices. + +.. code:: rust + + // TODO: remove this when you're done with your implementation. + #![allow(unused_variables, dead_code)] + + {{#include exercise.rs:transpose}} + unimplemented!() + } + + {{#include exercise.rs:tests}} + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/060_references.rst b/courses/comprehensive_rust_training/060_references.rst new file mode 100644 index 000000000..cf81e3e22 --- /dev/null +++ b/courses/comprehensive_rust_training/060_references.rst @@ -0,0 +1,44 @@ +************ +References +************ + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 060_references/01_shared.rst +.. include:: 060_references/02_exclusive.rst +.. include:: 060_references/03_slices.rst +.. include:: 060_references/04_strings.rst +.. include:: 060_references/05_dangling.rst +.. include:: 060_references/06_exercise.rst diff --git a/courses/comprehensive_rust_training/060_references/01_shared.rst b/courses/comprehensive_rust_training/060_references/01_shared.rst new file mode 100644 index 000000000..1cdf048b8 --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/01_shared.rst @@ -0,0 +1,66 @@ +=================== +Shared References +=================== + +------------------- +Shared References +------------------- + +A reference provides a way to access another value without taking +ownership of the value, and is also called :dfn:`borrowing`. Shared +references are read-only, and the referenced data cannot change. + +.. code:: rust + + fn main() { + let a = 'A'; + let b = 'B'; + let mut r: &char = &a; + println!("r: {}", *r); + r = &b; + println!("r: {}", *r); + } + +A shared reference to a type :rust:`T` has type :rust:`&T`. A reference value is +made with the :rust:`&` operator. The :rust:`*` operator :dfn:`dereferences` a +reference, yielding its value. + +--------- +Details +--------- + +- References can never be null in Rust, so null checking is not + necessary. + +- A reference is said to *borrow* the value it refers to, and this is a + good model for students not familiar with pointers: code can use the + reference to access the value, but is still **owned** by the original + variable. The course will get into more detail on ownership in day 3. + +- References are implemented as pointers, and a key advantage is that + they can be much smaller than the thing they point to. Students + familiar with C or C++ will recognize references as pointers. Later + parts of the course will cover how Rust prevents the memory-safety + bugs that come from using raw pointers. + +- Rust does not automatically create references for you - the :rust:`&` is + always required. + +- Rust will auto-dereference in some cases, in particular when invoking + methods (try :rust:`r.is_ascii()`). There is no need for an :rust:`->` + operator like in C++. + +- In this example, :rust:`r` is mutable so that it can be reassigned + (:rust:`r = &b`). Note that this re-binds :rust:`r`, so that it refers to + something else. This is different from C++, where assignment to a + reference changes the referenced value. + +- A shared reference does not allow modifying the value it refers to, + even if that value was mutable. Try :rust:`*r = 'X'`. + +- Rust is tracking the lifetimes of all references to ensure they live + long enough. Dangling references cannot occur in safe Rust. + :rust:`x_axis` would return a reference to :rust:`point`, but :rust:`point` will + be deallocated when the function returns, so this will not compile. + +- We will talk more about borrowing when we get to ownership. diff --git a/courses/comprehensive_rust_training/060_references/02_exclusive.rst b/courses/comprehensive_rust_training/060_references/02_exclusive.rst new file mode 100644 index 000000000..8aa49e915 --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/02_exclusive.rst @@ -0,0 +1,36 @@ +====================== +Exclusive References +====================== + +---------------------- +Exclusive References +---------------------- + +Exclusive references, also known as mutable references, allow changing +the value they refer to. They have type :rust:`&mut T`. + +.. code:: rust + + fn main() { + let mut point = (1, 2); + let x_coord = &mut point.0; + *x_coord = 20; + println!("point: {point:?}"); + } + +--------- +Details +--------- + +Key points: + +- :dfn:`Exclusive` means that only this reference can be used to access the + value. No other references (shared or exclusive) can exist at the + same time, and the referenced value cannot be accessed while the + exclusive reference exists. Try making a :rust:`&point.0` or changing + :rust:`point.0` while :rust:`x_coord` is alive. + +- Be sure to note the difference between :rust:`let mut x_coord: &i32` and + :rust:`let x_coord: &mut i32`. The first one represents a shared + reference which can be bound to different values, while the second + represents an exclusive reference to a mutable value. diff --git a/courses/comprehensive_rust_training/060_references/03_slices.rst b/courses/comprehensive_rust_training/060_references/03_slices.rst new file mode 100644 index 000000000..62fe9a5e5 --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/03_slices.rst @@ -0,0 +1,46 @@ +======== +Slices +======== + +-------- +Slices +-------- + +A slice gives you a view into a larger collection: + +.. code:: rust + + fn main() { + let a: [i32; 6] = [10, 20, 30, 40, 50, 60]; + println!("a: {a:?}"); + + let s: &[i32] = &a[2..4]; + + println!("s: {s:?}"); + } + +- Slices borrow data from the sliced type. + +--------- +Details +--------- + +- We create a slice by borrowing :rust:`a` and specifying the starting and + ending indexes in brackets. + +- If the slice starts at index 0, Rust's range syntax allows us to drop + the starting index, meaning that :rust:`&a[0..a.len()]` and + :rust:`&a[..a.len()]` are identical. + +- The same is true for the last index, so :rust:`&a[2..a.len()]` and + :rust:`&a[2..]` are identical. + +- To easily create a slice of the full array, we can therefore use + :rust:`&a[..]`. + +- :rust:`s` is a reference to a slice of :rust:`i32`. Notice that the type + of :rust:`s` (:rust:`&[i32]`) no longer mentions the array length. This + allows us to perform computation on slices of different sizes. + +- Slices always borrow from another object. In this example, :rust:`a` has + to remain 'alive' (in scope) for at least as long as our slice. diff --git a/courses/comprehensive_rust_training/060_references/04_strings.rst b/courses/comprehensive_rust_training/060_references/04_strings.rst new file mode 100644 index 000000000..18ba3b96d --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/04_strings.rst @@ -0,0 +1,80 @@ +========= +Strings +========= + +--------- +Strings +--------- + +We can now understand the two string types in Rust: + +- :rust:`&str` is a slice of UTF-8 encoded bytes, similar to :rust:`&[u8]`. +- :rust:`String` is an owned buffer of UTF-8 encoded bytes, similar to + :rust:`Vec`. + +.. code:: rust + + fn main() { + let s1: &str = "World"; + println!("s1: {s1}"); + + let mut s2: String = String::from("Hello "); + println!("s2: {s2}"); + s2.push_str(s1); + println!("s2: {s2}"); + + let s3: &str = &s2[s2.len() - s1.len()..]; + println!("s3: {s3}"); + } + +--------- +Details +--------- + +- :rust:`&str` introduces a string slice, which is an immutable reference + to UTF-8 encoded string data stored in a block of memory. String + literals (:rust:`"Hello"`), are stored in the program's binary. + +- Rust's :rust:`String` type is a wrapper around a vector of bytes. As with + a :rust:`Vec`, it is owned. + +- As with many other types :rust:`String::from()` creates a string from a + string literal; :rust:`String::new()` creates a new empty string, to + which string data can be added using the :rust:`push()` and + :rust:`push_str()` methods. + +- The :rust:`format!()` macro is a convenient way to generate an owned + string from dynamic values. It accepts the same format specification + as :rust:`println!()`. + +- You can borrow :rust:`&str` slices from :rust:`String` via :rust:`&` and + optionally range selection. If you select a byte range that is not + aligned to character boundaries, the expression will panic. The + :rust:`chars` iterator iterates over characters and is preferred over + trying to get character boundaries right. + +- For C++ programmers: think of :rust:`&str` as :rust:`std::string_view` from + C++, but the one that always points to a valid string in memory. Rust + :rust:`String` is a rough equivalent of :rust:`std::string` from C++ (main + difference: it can only contain UTF-8 encoded bytes and will never + use a small-string optimization). + +- Byte strings literals allow you to create a :rust:`&[u8]` value directly: + + .. code:: rust + + fn main() { + println!("{:?}", b"abc"); + println!("{:?}", &[97, 98, 99]); + } + +- Raw strings allow you to create a :rust:`&str` value with escapes + disabled: :rust:`r"\n" == "\\n"`. You can embed double-quotes by using an + equal amount of :rust:`#` on either side of the quotes: + + .. code:: rust + + fn main() { + println!(r#"link"#); + println!("link"); + } diff --git a/courses/comprehensive_rust_training/060_references/05_dangling.rst b/courses/comprehensive_rust_training/060_references/05_dangling.rst new file mode 100644 index 000000000..de790861f --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/05_dangling.rst @@ -0,0 +1,42 @@ +==================== +Reference Validity +==================== + +-------------------- +Reference Validity +-------------------- + +Rust enforces a number of rules for references that make them always +safe to use. One rule is that references can never be :rust:`null`, making +them safe to use without :rust:`null` checks. The other rule we'll look at +for now is that references can't *outlive* the data they point to. + +.. code:: rust + + fn main() { + let x_ref = { + let x = 10; + &x + }; + println!("x: {x_ref}"); + } + +--------- +Details +--------- + +- This slide gets students thinking about references as not simply + being pointers, since Rust has different rules for references than + other languages. + +- We'll look at the rest of Rust's borrowing rules on day 3 when we + talk about Rust's ownership system. + +----------------- +More to Explore +----------------- + +- Rust's equivalent of nullability is the :rust:`Option` type, which can be + used to make any type "nullable" (not just references/pointers). We + haven't yet introduced enums or pattern matching, though, so try not + to go into too much detail about this here. diff --git a/courses/comprehensive_rust_training/060_references/06_exercise.rst b/courses/comprehensive_rust_training/060_references/06_exercise.rst new file mode 100644 index 000000000..faf9f1b5c --- /dev/null +++ b/courses/comprehensive_rust_training/060_references/06_exercise.rst @@ -0,0 +1,34 @@ +==================== +Exercise: Geometry +==================== + +-------------------- +Exercise: Geometry +-------------------- + +We will create a few utility functions for 3-dimensional geometry, +representing a point as :rust:`[f64;3]`. It is up to you to determine the +function signatures. + +.. code:: rust + + // Calculate the magnitude of a vector by summing the squares of its coordinates + // and taking the square root. Use the `sqrt()` method to calculate the square + // root, like `v.sqrt()`. + + {{#include exercise.rs:magnitude}} + fn magnitude(...) -> f64 { + todo!() + } + + // Normalize a vector by calculating its magnitude and dividing all of its + // coordinates by that magnitude. + + {{#include exercise.rs:normalize}} + fn normalize(...) { + todo!() + } + + // Use the following `main` to test your work. + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/070_user_defined_types.rst b/courses/comprehensive_rust_training/070_user_defined_types.rst new file mode 100644 index 000000000..2823130b1 --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types.rst @@ -0,0 +1,45 @@ +******************** +User Defined Types +******************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 070_user_defined_types/01_named_structs.rst +.. include:: 070_user_defined_types/02_tuple_structs.rst +.. include:: 070_user_defined_types/03_enums.rst +.. include:: 070_user_defined_types/04_aliases.rst +.. include:: 070_user_defined_types/05_const.rst +.. include:: 070_user_defined_types/06_static.rst +.. include:: 070_user_defined_types/07_exercise.rst diff --git a/courses/comprehensive_rust_training/070_user_defined_types/01_named_structs.rst b/courses/comprehensive_rust_training/070_user_defined_types/01_named_structs.rst new file mode 100644 index 000000000..2d0a1fe06 --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/01_named_structs.rst @@ -0,0 +1,63 @@ +=============== +Named Structs +=============== + +--------------- +Named Structs +--------------- + +Like C and C++, Rust has support for custom structs: + +.. code:: rust + + struct Person { + name: String, + age: u8, + } + + fn describe(person: &Person) { + println!("{} is {} years old", person.name, person.age); + } + + fn main() { + let mut peter = Person { name: String::from("Peter"), age: 27 }; + describe(&peter); + + peter.age = 28; + describe(&peter); + + let name = String::from("Avery"); + let age = 39; + let avery = Person { name, age }; + describe(&avery); + + let jackie = Person { name: String::from("Jackie"), ..avery }; + describe(&jackie); + } + +--------- +Details +--------- + +Key Points: + +- Structs work like in C or C++. + + - Like in C++, and unlike in C, no typedef is needed to define a + type. + - Unlike in C++, there is no inheritance between structs. + +- This may be a good time to let people know there are different types + of structs. + + - Zero-sized structs (e.g. :rust:`struct Foo;`) might be used when + implementing a trait on some type but don't have any data that you + want to store in the value itself. + - The next slide will introduce Tuple structs, used when the field + names are not important. + +- If you already have variables with the right names, then you can + create the struct using a shorthand. +- The syntax :rust:`..avery` allows us to copy the majority of the fields + from the old struct without having to explicitly type it all out. It + must always be the last element. diff --git a/courses/comprehensive_rust_training/070_user_defined_types/02_tuple_structs.rst b/courses/comprehensive_rust_training/070_user_defined_types/02_tuple_structs.rst new file mode 100644 index 000000000..a473f1cfc --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/02_tuple_structs.rst @@ -0,0 +1,62 @@ +=============== +Tuple Structs +=============== + +--------------- +Tuple Structs +--------------- + +If the field names are unimportant, you can use a tuple struct: + +.. code:: rust + + struct Point(i32, i32); + + fn main() { + let p = Point(17, 23); + println!("({}, {})", p.0, p.1); + } + +This is often used for single-field wrappers (called :dfn:`newtypes`): + +.. code:: rust + + struct PoundsOfForce(f64); + struct Newtons(f64); + + fn compute_thruster_force() -> PoundsOfForce { + todo!("Ask a rocket scientist at NASA") + } + + fn set_thruster_force(force: Newtons) { + // ... + } + + fn main() { + let force = compute_thruster_force(); + set_thruster_force(force); + } + +--------- +Details +--------- + +- Newtypes are a great way to encode additional information about the + value in a primitive type, for example: + + - The number is measured in some units: :rust:`Newtons` in the example + above. + - The value passed some validation when it was created, so you no + longer have to validate it again at every use: + :rust:`PhoneNumber(String)` or :rust:`OddNumber(u32)`. + +- Demonstrate how to add a :rust:`f64` value to a :rust:`Newtons` type by + accessing the single field in the newtype. + + - Rust generally doesn't like inexplicit things, like automatic + unwrapping or for instance using booleans as integers. + - Operator overloading is discussed on Day 3 (generics). + +- The example is a subtle reference to the + `Mars Climate Orbiter `__ + failure. diff --git a/courses/comprehensive_rust_training/070_user_defined_types/03_enums.rst b/courses/comprehensive_rust_training/070_user_defined_types/03_enums.rst new file mode 100644 index 000000000..073166f40 --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/03_enums.rst @@ -0,0 +1,129 @@ +======= +Enums +======= + +------- +Enums +------- + +The :rust:`enum` keyword allows the creation of a type which has a few +different variants: + +.. code:: rust + + #[derive(Debug)] + enum Direction { + Left, + Right, + } + + #[derive(Debug)] + enum PlayerMove { + Pass, // Simple variant + Run(Direction), // Tuple variant + Teleport { x: u32, y: u32 }, // Struct variant + } + + fn main() { + let player_move: PlayerMove = PlayerMove::Run(Direction::Left); + println!("On this turn: {player_move:?}"); + } + +--------- +Details +--------- + +Key Points: + +- Enumerations allow you to collect a set of values under one type. +- :rust:`Direction` is a type with variants. There are two values of + :rust:`Direction`: :rust:`Direction::Left` and :rust:`Direction::Right`. +- :rust:`PlayerMove` is a type with three variants. In addition to the + payloads, Rust will store a discriminant so that it knows at runtime + which variant is in a :rust:`PlayerMove` value. +- This might be a good time to compare structs and enums: + + - In both, you can have a simple version without fields (unit + struct) or one with different types of fields (variant payloads). + - You could even implement the different variants of an enum with + separate structs but then they wouldn't be the same type as they + would if they were all defined in an enum. + +- Rust uses minimal space to store the discriminant. + + - If necessary, it stores an integer of the smallest required size + + - If the allowed variant values do not cover all bit patterns, it + will use invalid bit patterns to encode the discriminant (the + "niche optimization"). For example, :rust:`Option<&u8>` stores either + a pointer to an integer or :rust:`NULL` for the :rust:`None` variant. + + - You can control the discriminant if needed (e.g., for + compatibility with C): + + .. code:: rust + + #[repr(u32)] + enum Bar { + A, // 0 + B = 10000, + C, // 10001 + } + + fn main() { + println!("A: {}", Bar::A as u32); + println!("B: {}", Bar::B as u32); + println!("C: {}", Bar::C as u32); + } + + Without :rust:`repr`, the discriminant type takes 2 bytes, because + 10001 fits 2 bytes. + +----------------- +More to Explore +----------------- + +Rust has several optimizations it can employ to make enums take up less +space. + +- Null pointer optimization: For + `some types `__, Rust + guarantees that :rust:`size_of::()` equals :rust:`size_of::>()`. + + Example code if you want to show how the bitwise representation *may* + look like in practice. It's important to note that the compiler + provides no guarantees regarding this representation, therefore this + is totally unsafe. + + .. code:: rust + + use std::mem::transmute; + + macro_rules! dbg_bits { + ($e:expr, $bit_type:ty) => { + println!("- {}: {:#x}", stringify!($e), transmute::<_, $bit_type>($e)); + }; + } + + fn main() { + unsafe { + println!("bool:"); + dbg_bits!(false, u8); + dbg_bits!(true, u8); + + println!("Option:"); + dbg_bits!(None::, u8); + dbg_bits!(Some(false), u8); + dbg_bits!(Some(true), u8); + + println!("Option>:"); + dbg_bits!(Some(Some(false)), u8); + dbg_bits!(Some(Some(true)), u8); + dbg_bits!(Some(None::), u8); + dbg_bits!(None::>, u8); + + println!("Option<&i32>:"); + dbg_bits!(None::<&i32>, usize); + dbg_bits!(Some(&0i32), usize); + } + } diff --git a/courses/comprehensive_rust_training/070_user_defined_types/04_aliases.rst b/courses/comprehensive_rust_training/070_user_defined_types/04_aliases.rst new file mode 100644 index 000000000..21fd65cbf --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/04_aliases.rst @@ -0,0 +1,34 @@ +============== +Type Aliases +============== + +-------------- +Type Aliases +-------------- + +A type alias creates a name for another type. The two types can be used +interchangeably. + +.. code:: rust + + enum CarryableConcreteItem { + Left, + Right, + } + + type Item = CarryableConcreteItem; + + // Aliases are more useful with long, complex types: + use std::cell::RefCell; + use std::sync::{Arc, RwLock}; + type PlayerInventory = RwLock>>>; + +--------- +Details +--------- + +- A `newtype `__ is often a better alternative + since it creates a distinct type. Prefer + :rust:`struct InventoryCount(usize)` to :rust:`type InventoryCount = usize`. + +- C programmers will recognize this as similar to a :cpp:`typedef`. diff --git a/courses/comprehensive_rust_training/070_user_defined_types/05_const.rst b/courses/comprehensive_rust_training/070_user_defined_types/05_const.rst new file mode 100644 index 000000000..d3fb029fa --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/05_const.rst @@ -0,0 +1,51 @@ +=========== +"const" +=========== + +----------- +"const" +----------- + +Constants are evaluated at compile time and their values are inlined +wherever they are used: + +.. code:: rust + + const DIGEST_SIZE: usize = 3; + const FILL_VALUE: u8 = calculate_fill_value(); + + const fn calculate_fill_value() -> u8 { + if DIGEST_SIZE < 10 { + 42 + } else { + 13 + } + } + + fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] { + let mut digest = [FILL_VALUE; DIGEST_SIZE]; + for (idx, &b) in text.as_bytes().iter().enumerate() { + digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b); + } + digest + } + + fn main() { + let digest = compute_digest("Hello"); + println!("digest: {digest:?}"); + } + +According to the +`Rust RFC Book `__ +these are inlined upon use. + +Only functions marked :rust:`const` can be called at compile time to +generate :rust:`const` values. :rust:`const` functions can however be called at +runtime. + +--------- +Details +--------- + +- Mention that :rust:`const` behaves semantically similar to C++'s + :cpp:`constexpr` diff --git a/courses/comprehensive_rust_training/070_user_defined_types/06_static.rst b/courses/comprehensive_rust_training/070_user_defined_types/06_static.rst new file mode 100644 index 000000000..7275548b4 --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/06_static.rst @@ -0,0 +1,53 @@ +============ +"static" +============ + +------------ +"static" +------------ + +Static variables will live during the whole execution of the program, +and therefore will not move: + +.. code:: rust + + static BANNER: &str = "Welcome to RustOS 3.14"; + + fn main() { + println!("{BANNER}"); + } + +As noted in the +`Rust RFC Book `__, +these are not inlined upon use and have an actual associated memory +location. This is useful for unsafe and embedded code, and the variable +lives through the entirety of the program execution. When a +globally-scoped value does not have a reason to need object identity, +:rust:`const` is generally preferred. + +--------- +Details +--------- + +- :rust:`static` is similar to mutable global variables in C++. +- :rust:`static` provides object identity: an address in memory and state + as required by types with interior mutability such as :rust:`Mutex`. + +================= +More to Explore +================= + +----------------- +More to Explore +----------------- + +Because :rust:`static` variables are accessible from any thread, they must +be :rust:`Sync`. Interior mutability is possible through a +`Mutex `__, +atomic or similar. + +It is common to use :rust:`OnceLock` in a static as a way to support +initialization on first use. :rust:`OnceCell` is not :rust:`Sync` and thus +cannot be used in this context. + +Thread-local data can be created with the macro :rust:`std::thread_local`. diff --git a/courses/comprehensive_rust_training/070_user_defined_types/07_exercise.rst b/courses/comprehensive_rust_training/070_user_defined_types/07_exercise.rst new file mode 100644 index 000000000..bbb5c104e --- /dev/null +++ b/courses/comprehensive_rust_training/070_user_defined_types/07_exercise.rst @@ -0,0 +1,46 @@ +=========================== +Exercise: Elevator Events +=========================== + +--------------------------- +Exercise: Elevator Events +--------------------------- + +We will create a data structure to represent an event in an elevator +control system. It is up to you to define the types and functions to +construct various events. Use :rust:`#[derive(Debug)]` to allow the types to +be formatted with :rust:`{:?}`. + +This exercise only requires creating and populating data structures so +that :rust:`main` runs without errors. The next part of the course will +cover getting data out of these structures. + +.. code:: rust + + {{#include exercise.rs:event}} + // TODO: add required variants + } + + {{#include exercise.rs:direction}} + + {{#include exercise.rs:car_arrived}} + todo!() + } + + {{#include exercise.rs:car_door_opened}} + todo!() + } + + {{#include exercise.rs:car_door_closed}} + todo!() + } + + {{#include exercise.rs:lobby_call_button_pressed}} + todo!() + } + + {{#include exercise.rs:car_floor_button_pressed}} + todo!() + } + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/080_pattern_matching.rst b/courses/comprehensive_rust_training/080_pattern_matching.rst new file mode 100644 index 000000000..6104446e0 --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching.rst @@ -0,0 +1,43 @@ +****************** +Pattern Matching +****************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 080_pattern_matching/01_match.rst +.. include:: 080_pattern_matching/02_destructuring_structs.rst +.. include:: 080_pattern_matching/03_destructuring_enums.rst +.. include:: 080_pattern_matching/04_let_control_flow.rst +.. include:: 080_pattern_matching/05_exercise.rst diff --git a/courses/comprehensive_rust_training/080_pattern_matching/01_match.rst b/courses/comprehensive_rust_training/080_pattern_matching/01_match.rst new file mode 100644 index 000000000..15dbe812d --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching/01_match.rst @@ -0,0 +1,85 @@ +================= +Matching Values +================= + +----------------- +Matching Values +----------------- + +The :rust:`match` keyword lets you match a value against one or more +*patterns*. The patterns can be simple values, similarly to :cpp:`switch` +in C and C++, but they can also be used to express more complex +conditions: + +.. code:: rust + + #[rustfmt::skip] + fn main() { + let input = 'x'; + match input { + 'q' => println!("Quitting"), + 'a' | 's' | 'w' | 'd' => println!("Moving around"), + '0'..='9' => println!("Number input"), + key if key.is_lowercase() => println!("Lowercase: {key}"), + _ => println!("Something else"), + } + } + +A variable in the pattern (:rust:`key` in this example) will create a +binding that can be used within the match arm. We will learn more about +this on the next slide. + +A match guard causes the arm to match only if the condition is true. If +the condition is false the match will continue checking later cases. + +--------- +Details +--------- + +Key Points: + +- You might point out how some specific characters are being used when + in a pattern + + - :rust:`|` as an :rust:`or` + - :rust:`..` can expand as much as it needs to be + - :rust:`1..=5` represents an inclusive range + - :rust:`_` is a wild card + +- Match guards as a separate syntax feature are important and necessary + when we wish to concisely express more complex ideas than patterns + alone would allow. +- They are not the same as separate :rust:`if` expression inside of the + match arm. An :rust:`if` expression inside of the branch block (after + :rust:`=>`) happens after the match arm is selected. Failing the :rust:`if` + condition inside of that block won't result in other arms of the + original :rust:`match` expression being considered. +- The condition defined in the guard applies to every expression in a + pattern with an :rust:`|`. + +================= +More To Explore +================= + +----------------- +More To Explore +----------------- + +- Another piece of pattern syntax you can show students is the :rust:`@` + syntax which binds a part of a pattern to a variable. For example: + + .. code:: rust + + let opt = Some(123); + match opt { + outer @ Some(inner) => { + println!("outer: {outer:?}, inner: {inner}"); + } + None => {} + } + + In this example :rust:`inner` has the value 123 which it pulled from the + :rust:`Option` via destructuring, :rust:`outer` captures the entire + :rust:`Some(inner)` expression, so it contains the full + :rust:`Option::Some(123)`. This is rarely used but can be useful in more + complex patterns. diff --git a/courses/comprehensive_rust_training/080_pattern_matching/02_destructuring_structs.rst b/courses/comprehensive_rust_training/080_pattern_matching/02_destructuring_structs.rst new file mode 100644 index 000000000..7f6ba5a8a --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching/02_destructuring_structs.rst @@ -0,0 +1,25 @@ +========= +Structs +========= + +--------- +Structs +--------- + +Like tuples, Struct can also be destructured by matching: + +.. code:: rust + + {{#include ../../third_party/rust-by-example/destructuring-structs.rs}} + +--------- +Details +--------- + +- Change the literal values in :rust:`foo` to match with the other + patterns. +- Add a new field to :rust:`Foo` and make changes to the pattern as needed. +- The distinction between a capture and a constant expression can be + hard to spot. Try changing the :rust:`2` in the second arm to a variable, + and see that it subtly doesn't work. Change it to a :rust:`const` and see + it working again. diff --git a/courses/comprehensive_rust_training/080_pattern_matching/03_destructuring_enums.rst b/courses/comprehensive_rust_training/080_pattern_matching/03_destructuring_enums.rst new file mode 100644 index 000000000..29d0ba1b9 --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching/03_destructuring_enums.rst @@ -0,0 +1,56 @@ +======= +Enums +======= + +------- +Enums +------- + +Like tuples, enums can also be destructured by matching: + +Patterns can also be used to bind variables to parts of your values. +This is how you inspect the structure of your types. Let us start with a +simple :rust:`enum` type: + +.. code:: rust + + enum Result { + Ok(i32), + Err(String), + } + + fn divide_in_two(n: i32) -> Result { + if n % 2 == 0 { + Result::Ok(n / 2) + } else { + Result::Err(format!("cannot divide {n} into two equal parts")) + } + } + + fn main() { + let n = 100; + match divide_in_two(n) { + Result::Ok(half) => println!("{n} divided in two is {half}"), + Result::Err(msg) => println!("sorry, an error happened: {msg}"), + } + } + +Here we have used the arms to *destructure* the :rust:`Result` value. In the +first arm, :rust:`half` is bound to the value inside the :rust:`Ok` variant. In +the second arm, :rust:`msg` is bound to the error message. + +--------- +Details +--------- + +- The :rust:`if`/:rust:`else` expression is returning an enum that is later + unpacked with a :rust:`match`. +- You can try adding a third variant to the enum definition and + displaying the errors when running the code. Point out the places + where your code is now inexhaustive and how the compiler tries to + give you hints. +- The values in the enum variants can only be accessed after being + pattern matched. +- Demonstrate what happens when the search is inexhaustive. Note the + advantage the Rust compiler provides by confirming when all cases are + handled. diff --git a/courses/comprehensive_rust_training/080_pattern_matching/04_let_control_flow.rst b/courses/comprehensive_rust_training/080_pattern_matching/04_let_control_flow.rst new file mode 100644 index 000000000..d38b5c052 --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching/04_let_control_flow.rst @@ -0,0 +1,147 @@ +================== +Let Control Flow +================== + +------------------ +Let Control Flow +------------------ + +Rust has a few control flow constructs which differ from other +languages. They are used for pattern matching: + +- :rust:`if let` expressions +- :rust:`let else` expressions +- :rust:`while let` expressions + +------------------------ +"if let" expressions +------------------------ + +The +`if let expression `__ +lets you execute different code depending on whether a value matches a +pattern: + +.. code:: rust + + use std::time::Duration; + + fn sleep_for(secs: f32) { + if let Ok(duration) = Duration::try_from_secs_f32(secs) { + std::thread::sleep(duration); + println!("slept for {duration:?}"); + } + } + + fn main() { + sleep_for(-10.0); + sleep_for(0.8); + } + +---------------- +if-let Details +---------------- + +- Unlike :rust:`match`, :rust:`if let` does not have to cover all branches. + This can make it more concise than :rust:`match`. +- A common usage is handling :rust:`Some` values when working with + :rust:`Option`. +- Unlike :rust:`match`, :rust:`if let` does not support guard clauses for + pattern matching. + +------------------------ +"while let" Statements +------------------------ + +Like with :rust:`if let`, there is a +`while let `__ +variant which repeatedly tests a value against a pattern: + +.. code:: rust + + fn main() { + let mut name = String::from("Comprehensive Rust"); + while let Some(c) = name.pop() { + println!("character: {c}"); + } + // (There are more efficient ways to reverse a string!) + } + +Here +`String::pop `__ +returns :rust:`Some(c)` until the string is empty, after which it will +return :rust:`None`. The :rust:`while let` lets us keep iterating through all +items. + +------------------- +while-let Details +------------------- + +- Point out that the :rust:`while let` loop will keep going as long as the + value matches the pattern. +- You could rewrite the :rust:`while let` loop as an infinite loop with an + if statement that breaks when there is no value to unwrap for + :rust:`name.pop()`. The :rust:`while let` provides syntactic sugar for the + above scenario. + +-------------------------- +"let else" expressions +-------------------------- + +For the common case of matching a pattern and returning from the +function, use +`let else `__. +The "else" case must diverge (:rust:`return`, :rust:`break`, or panic - anything +but falling off the end of the block). + +.. code:: rust + + fn hex_or_die_trying(maybe_string: Option) -> Result { + // TODO: The structure of this code is difficult to follow -- rewrite it with let-else! + if let Some(s) = maybe_string { + if let Some(first_byte_char) = s.chars().next() { + if let Some(digit) = first_byte_char.to_digit(16) { + Ok(digit) + } else { + return Err(String::from("not a hex digit")); + } + } else { + return Err(String::from("got empty string")); + } + } else { + return Err(String::from("got None")); + } + } + + fn main() { + println!("result: {:?}", hex_or_die_trying(Some(String::from("foo")))); + } + + +------------------ +let-else Details +------------------ + +:rust:`if-let` can pile up, as shown. The :rust:`let-else` construct supports +flattening this nested code. Rewrite the awkward version for students, +so they can see the transformation. + +The rewritten version is: + +.. code:: rust + + fn hex_or_die_trying(maybe_string: Option) -> Result { + let Some(s) = maybe_string else { + return Err(String::from("got None")); + }; + + let Some(first_byte_char) = s.chars().next() else { + return Err(String::from("got empty string")); + }; + + let Some(digit) = first_byte_char.to_digit(16) else { + return Err(String::from("not a hex digit")); + }; + + return Ok(digit); + } diff --git a/courses/comprehensive_rust_training/080_pattern_matching/05_exercise.rst b/courses/comprehensive_rust_training/080_pattern_matching/05_exercise.rst new file mode 100644 index 000000000..bc2c3fe0b --- /dev/null +++ b/courses/comprehensive_rust_training/080_pattern_matching/05_exercise.rst @@ -0,0 +1,80 @@ +================================= +Exercise: Expression Evaluation +================================= + +--------------------------------- +Exercise: Expression Evaluation +--------------------------------- + +Let's write a simple recursive evaluator for arithmetic expressions. + +An example of a small arithmetic expression could be :rust:`10 + 20`, which +evaluates to :rust:`30`. We can represent the expression as a tree: + +.. code:: bob + + .-------. + .------ | + | ------. + | '-------' | + v v + .--------. .--------. + | 10 | | 20 | + '--------' '--------' + +A bigger and more complex expression would be +:rust:`(10 * 9) + ((3 - 4) * 5)`, which evaluate to :rust:`85`. We represent +this as a much bigger tree: + +.. code:: bob + + .-----. + .---------------- | + | ----------------. + | '-----' | + v v + .-----. .-----. + .---- | * | ----. .---- | * | ----. + | '-----' | | '-----' | + v v v v + .------. .-----. .-----. .-----. + | 10 | | 9 | .---- | "-"| ----. | 5 | + '------' '-----' | '-----' | '-----' + v v + .-----. .-----. + | 3 | | 4 | + '-----' '-----' + +In code, we will represent the tree with two types: + +.. code:: rust + + {{#include exercise.rs:Operation}} + + {{#include exercise.rs:Expression}} + +The :rust:`Box` type here is a smart pointer, and will be covered in detail +later in the course. An expression can be "boxed" with :rust:`Box::new` as +seen in the tests. To evaluate a boxed expression, use the deref +operator (:rust:`*`) to "unbox" it: :rust:`eval(*boxed_expr)`. + +Copy and paste the code into the Rust playground, and begin implementing +:rust:`eval`. The final product should pass the tests. It may be helpful to +use :rust:`todo!()` and get the tests to pass one-by-one. You can also skip +a test temporarily with :rust:`#[ignore]`: + +.. code:: none + + #[test] + #[ignore] + fn test_value() { .. } + +.. code:: rust + + {{#include exercise.rs:Operation}} + + {{#include exercise.rs:Expression}} + + {{#include exercise.rs:eval}} + todo!() + } + + {{#include exercise.rs:tests}} diff --git a/courses/comprehensive_rust_training/090_methods_and_traits.rst b/courses/comprehensive_rust_training/090_methods_and_traits.rst new file mode 100644 index 000000000..d2ae4c12f --- /dev/null +++ b/courses/comprehensive_rust_training/090_methods_and_traits.rst @@ -0,0 +1,42 @@ +******************** +Methods And Traits +******************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 090_methods_and_traits/01_methods.rst +.. include:: 090_methods_and_traits/02_traits.rst +.. include:: 090_methods_and_traits/03_deriving.rst +.. include:: 090_methods_and_traits/04_exercise.rst diff --git a/courses/comprehensive_rust_training/090_methods_and_traits/01_methods.rst b/courses/comprehensive_rust_training/090_methods_and_traits/01_methods.rst new file mode 100644 index 000000000..d32de0718 --- /dev/null +++ b/courses/comprehensive_rust_training/090_methods_and_traits/01_methods.rst @@ -0,0 +1,101 @@ +========= +Methods +========= + +--------- +Methods +--------- + +Rust allows you to associate functions with your new types. You do this +with an :rust:`impl` block: + +.. code:: rust + + #[derive(Debug)] + struct CarRace { + name: String, + laps: Vec, + } + + impl CarRace { + // No receiver, a static method + fn new(name: &str) -> Self { + Self { name: String::from(name), laps: Vec::new() } + } + + // Exclusive borrowed read-write access to self + fn add_lap(&mut self, lap: i32) { + self.laps.push(lap); + } + + // Shared and read-only borrowed access to self + fn print_laps(&self) { + println!("Recorded {} laps for {}:", self.laps.len(), self.name); + for (idx, lap) in self.laps.iter().enumerate() { + println!("Lap {idx}: {lap} sec"); + } + } + + // Exclusive ownership of self (covered later) + fn finish(self) { + let total: i32 = self.laps.iter().sum(); + println!("Race {} is finished, total lap time: {}", self.name, total); + } + } + + fn main() { + let mut race = CarRace::new("Monaco Grand Prix"); + race.add_lap(70); + race.add_lap(68); + race.print_laps(); + race.add_lap(71); + race.print_laps(); + race.finish(); + // race.add_lap(42); + } + +The :rust:`self` arguments specify the "receiver" - the object the method +acts on. There are several common receivers for a method: + +- :rust:`&self`: borrows the object from the caller using a shared and + immutable reference. The object can be used again afterwards. +- :rust:`&mut self`: borrows the object from the caller using a unique and + mutable reference. The object can be used again afterwards. +- :rust:`self`: takes ownership of the object and moves it away from the + caller. The method becomes the owner of the object. The object will + be dropped (deallocated) when the method returns, unless its + ownership is explicitly transmitted. Complete ownership does not + automatically mean mutability. +- :rust:`mut self`: same as above, but the method can mutate the object. +- No receiver: this becomes a static method on the struct. Typically + used to create constructors which are called :rust:`new` by convention. + +--------- +Details +--------- + +Key Points: + +- It can be helpful to introduce methods by comparing them to + functions. + + - Methods are called on an instance of a type (such as a struct or + enum), the first parameter represents the instance as :rust:`self`. + - Developers may choose to use methods to take advantage of method + receiver syntax and to help keep them more organized. By using + methods we can keep all the implementation code in one predictable + place. + +- Point out the use of the keyword :rust:`self`, a method receiver. + + - Show that it is an abbreviated term for :rust:`self: Self` and perhaps + show how the struct name could also be used. + - Explain that :rust:`Self` is a type alias for the type the :rust:`impl` + block is in and can be used elsewhere in the block. + - Note how :rust:`self` is used like other structs and dot notation can + be used to refer to individual fields. + - This might be a good time to demonstrate how the :rust:`&self` differs + from :rust:`self` by trying to run :rust:`finish` twice. + - Beyond variants on :rust:`self`, there are also + `special wrapper types `__ + allowed to be receiver types, such as :rust:`Box`. diff --git a/courses/comprehensive_rust_training/090_methods_and_traits/02_traits.rst b/courses/comprehensive_rust_training/090_methods_and_traits/02_traits.rst new file mode 100644 index 000000000..6d9caf4e4 --- /dev/null +++ b/courses/comprehensive_rust_training/090_methods_and_traits/02_traits.rst @@ -0,0 +1,159 @@ +======== +Traits +======== + +-------- +Traits +-------- + +Rust lets you abstract over types with traits. They're similar to +interfaces: + +.. code:: rust + + trait Pet { + /// Return a sentence from this pet. + fn talk(&self) -> String; + + /// Print a string to the terminal greeting this pet. + fn greet(&self); + } + +--------- +Details +--------- + +- A trait defines a number of methods that types must have in order to + implement the trait. + +- In the **Generics** segment, next, we will see how to build + functionality that is generic over all types implementing a trait. + +--------------------- +Implementing Traits +--------------------- + +.. code:: rust + + trait Pet { + fn talk(&self) -> String; + + fn greet(&self) { + println!("Oh you're a cutie! What's your name? {}", self.talk()); + } + } + + struct Dog { + name: String, + age: i8, + } + + impl Pet for Dog { + fn talk(&self) -> String { + format!("Woof, my name is {}!", self.name) + } + } + + fn main() { + let fido = Dog { name: String::from("Fido"), age: 5 }; + fido.greet(); + } + +----------------------------- +Implementing Traits Details +----------------------------- + +- To implement :rust:`Trait` for :rust:`Type`, you use an :rust:`impl Trait for Type { .. }` + block. + +- Unlike Go interfaces, just having matching methods is not enough: a :rust:`Cat` type + with a :rust:`talk()` method would not automatically satisfy :rust:`Pet` unless it is in + an :rust:`impl Pet` block. + +- Traits may provide default implementations of some methods. Default + implementations can rely on all the methods of the trait. In this case, + :rust:`greet` is provided, and relies on :rust:`talk`. + +------------- +Supertraits +------------- + +A trait can require that types implementing it also implement other traits, +called :dfn:`supertraits`. Here, any type implementing :rust:`Pet` must implement :rust:`Animal`. + +.. code:: rust + + trait Animal { + fn leg_count(&self) -> u32; + } + + trait Pet: Animal { + fn name(&self) -> String; + } + + struct Dog(String); + + impl Animal for Dog { + fn leg_count(&self) -> u32 { + 4 + } + } + + impl Pet for Dog { + fn name(&self) -> String { + self.0.clone() + } + } + + fn main() { + let puppy = Dog(String::from("Rex")); + println!("{} has {} legs", puppy.name(), puppy.leg_count()); + } + +--------------------- +Supertraits Details +--------------------- + +This is sometimes called *trait inheritance* but students should not expect this +to behave like OO inheritance. It just specifies an additional requirement on +implementations of a trait. + +------------------ +Associated Types +------------------ + +Associated types are placeholder types which are supplied by the trait +implementation. + +.. code:: rust + + #[derive(Debug)] + struct Meters(i32); + #[derive(Debug)] + struct MetersSquared(i32); + + trait Multiply { + type Output; + fn multiply(&self, other: &Self) -> Self::Output; + } + + impl Multiply for Meters { + type Output = MetersSquared; + fn multiply(&self, other: &Self) -> Self::Output { + MetersSquared(self.0 * other.0) + } + } + + fn main() { + println!("{:?}", Meters(10).multiply(&Meters(20))); + } + +-------------------------- +Associated Types Details +-------------------------- + +- Associated types are sometimes also called :dfn:`output types`. The key observation + is that the implementer, not the caller, chooses this type. + +- Many standard library traits have associated types, including arithmetic + operators and :rust:`Iterator`. diff --git a/courses/comprehensive_rust_training/090_methods_and_traits/03_deriving.rst b/courses/comprehensive_rust_training/090_methods_and_traits/03_deriving.rst new file mode 100644 index 000000000..10a5b472f --- /dev/null +++ b/courses/comprehensive_rust_training/090_methods_and_traits/03_deriving.rst @@ -0,0 +1,36 @@ +========== +Deriving +========== + +---------- +Deriving +---------- + +Supported traits can be automatically implemented for your custom types, +as follows: + +.. code:: rust + + #[derive(Debug, Clone, Default)] + struct Player { + name: String, + strength: u8, + hit_points: u8, + } + + fn main() { + let p1 = Player::default(); // Default trait adds `default` constructor. + let mut p2 = p1.clone(); // Clone trait adds `clone` method. + p2.name = String::from("EldurScrollz"); + // Debug trait adds support for printing with `{:?}`. + println!("{p1:?} vs. {p2:?}"); + } + +--------- +Details +--------- + +Derivation is implemented with macros, and many crates provide useful +derive macros to add useful functionality. For example, :rust:`serde` can +derive serialization support for a struct using +:rust:`#[derive(Serialize)]`. diff --git a/courses/comprehensive_rust_training/090_methods_and_traits/04_exercise.rst b/courses/comprehensive_rust_training/090_methods_and_traits/04_exercise.rst new file mode 100644 index 000000000..b33499f1e --- /dev/null +++ b/courses/comprehensive_rust_training/090_methods_and_traits/04_exercise.rst @@ -0,0 +1,29 @@ +======================== +Exercise: Logger Trait +======================== + +------------------------ +Exercise: Logger Trait +------------------------ + +Let's design a simple logging utility, using a trait :rust:`Logger` with a +:rust:`log` method. Code which might log its progress can then take an +:rust:`&impl Logger`. In testing, this might put messages in the test +logfile, while in a production build it would send messages to a log +server. + +However, the :rust:`StderrLogger` given below logs all messages, regardless +of verbosity. Your task is to write a :rust:`VerbosityFilter` type that will +ignore messages above a maximum verbosity. + +This is a common pattern: a struct wrapping a trait implementation and +implementing that same trait, adding behavior in the process. What other +kinds of wrappers might be useful in a logging utility? + +.. code:: rust + + {{#include exercise.rs:setup}} + + // TODO: Define and implement `VerbosityFilter`. + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/100_generics.rst b/courses/comprehensive_rust_training/100_generics.rst new file mode 100644 index 000000000..a08b63e3f --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics.rst @@ -0,0 +1,45 @@ +********** +Generics +********** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 100_generics/01_generic_functions.rst +.. include:: 100_generics/02_generic_data.rst +.. include:: 100_generics/03_generic_traits.rst +.. include:: 100_generics/04_trait_bounds.rst +.. include:: 100_generics/05_impl_trait.rst +.. include:: 100_generics/06_dyn_trait.rst +.. include:: 100_generics/07_exercise.rst diff --git a/courses/comprehensive_rust_training/100_generics/01_generic_functions.rst b/courses/comprehensive_rust_training/100_generics/01_generic_functions.rst new file mode 100644 index 000000000..127ae8ad3 --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/01_generic_functions.rst @@ -0,0 +1,57 @@ +=================== +Generic Functions +=================== + +------------------- +Generic Functions +------------------- + +Rust supports generics, which lets you abstract algorithms or data +structures (such as sorting or a binary tree) over the types used or +stored. + +.. code:: rust + + /// Pick `even` or `odd` depending on the value of `n`. + fn pick(n: i32, even: T, odd: T) -> T { + if n % 2 == 0 { + even + } else { + odd + } + } + + fn main() { + println!("picked a number: {:?}", pick(97, 222, 333)); + println!("picked a string: {:?}", pick(28, "dog", "cat")); + } + +--------- +Details +--------- + +- Rust infers a type for T based on the types of the arguments and + return value. + +- In this example we only use the primitive types :rust:`i32` and :rust:`&str` + for :rust:`T`, but we can use any type here, including user-defined + types: + + .. code:: rust + + struct Foo { + val: u8, + } + + pick(123, Foo { val: 7 }, Foo { val: 456 }); + +- This is similar to C++ templates, but Rust partially compiles the + generic function immediately, so that function must be valid for all + types matching the constraints. For example, try modifying :rust:`pick` + to return :rust:`even + odd` if :rust:`n == 0`. Even if only the :rust:`pick` + instantiation with integers is used, Rust still considers it invalid. + C++ would let you do this. + +- Generic code is turned into non-generic code based on the call sites. + This is a zero-cost abstraction: you get exactly the same result as + if you had hand-coded the data structures without the abstraction. diff --git a/courses/comprehensive_rust_training/100_generics/02_generic_data.rst b/courses/comprehensive_rust_training/100_generics/02_generic_data.rst new file mode 100644 index 000000000..d34b387aa --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/02_generic_data.rst @@ -0,0 +1,54 @@ +==================== +Generic Data Types +==================== + +-------------------- +Generic Data Types +-------------------- + +You can use generics to abstract over the concrete field type: + +.. code:: rust + + #[derive(Debug)] + struct Point { + x: T, + y: T, + } + + impl Point { + fn coords(&self) -> (&T, &T) { + (&self.x, &self.y) + } + + fn set_x(&mut self, x: T) { + self.x = x; + } + } + + fn main() { + let integer = Point { x: 5, y: 10 }; + let float = Point { x: 1.0, y: 4.0 }; + println!("{integer:?} and {float:?}"); + println!("coords: {:?}", integer.coords()); + } + +--------- +Details +--------- + +- *Q:* Why :rust:`T` is specified twice in :rust:`impl Point {}`? Isn't + that redundant? + + - This is because it is a generic implementation section for generic + type. They are independently generic. + - It means these methods are defined for any :rust:`T`. + - It is possible to write :rust:`impl Point { .. }`. + + - :rust:`Point` is still generic and you can use :rust:`Point`, but + methods in this block will only be available for + :rust:`Point`. + +- Try declaring a new variable :rust:`let p = Point { x: 5, y: 10.0 };`. + Update the code to allow points that have elements of different + types, by using two type variables, e.g., :rust:`T` and :rust:`U`. diff --git a/courses/comprehensive_rust_training/100_generics/03_generic_traits.rst b/courses/comprehensive_rust_training/100_generics/03_generic_traits.rst new file mode 100644 index 000000000..032d8fd9c --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/03_generic_traits.rst @@ -0,0 +1,55 @@ +================ +Generic Traits +================ + +---------------- +Generic Traits +---------------- + +Traits can also be generic, just like types and functions. A trait's +parameters get concrete types when it is used. + +.. code:: rust + + #[derive(Debug)] + struct Foo(String); + + impl From for Foo { + fn from(from: u32) -> Foo { + Foo(format!("Converted from integer: {from}")) + } + } + + impl From for Foo { + fn from(from: bool) -> Foo { + Foo(format!("Converted from bool: {from}")) + } + } + + fn main() { + let from_int = Foo::from(123); + let from_bool = Foo::from(true); + println!("{from_int:?}, {from_bool:?}"); + } + +--------- +Details +--------- + +- The :rust:`From` trait will be covered later in the course, but its + `definition in the std docs `__ + is simple. + +- Implementations of the trait do not need to cover all possible type + parameters. Here, :rust:`Foo::from("hello")` would not compile because + there is no :rust:`From<&str>` implementation for :rust:`Foo`. + +- Generic traits take types as "input", while associated types are a + kind of "output" type. A trait can have multiple implementations for + different input types. + +- In fact, Rust requires that at most one implementation of a trait + match for any type T. Unlike some other languages, Rust has no + heuristic for choosing the "most specific" match. There is work on + adding this support, called + `specialization `__. diff --git a/courses/comprehensive_rust_training/100_generics/04_trait_bounds.rst b/courses/comprehensive_rust_training/100_generics/04_trait_bounds.rst new file mode 100644 index 000000000..724dfe5f2 --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/04_trait_bounds.rst @@ -0,0 +1,56 @@ +============== +Trait Bounds +============== + +-------------- +Trait Bounds +-------------- + +When working with generics, you often want to require the types to +implement some trait, so that you can call this trait's methods. + +You can do this with :rust:`T: Trait`: + +.. code:: rust + + fn duplicate(a: T) -> (T, T) { + (a.clone(), a.clone()) + } + + struct NotCloneable; + + fn main() { + let foo = String::from("foo"); + let pair = duplicate(foo); + println!("{pair:?}"); + } + +--------- +Details +--------- + +- Try making a :rust:`NonCloneable` and passing it to :rust:`duplicate`. + +- When multiple traits are necessary, use :rust:`+` to join them. + +- Show a :rust:`where` clause, students will encounter it when reading + code. + + .. code:: rust + + fn duplicate(a: T) -> (T, T) + where + T: Clone, + { + (a.clone(), a.clone()) + } + + - It declutters the function signature if you have many parameters. + - It has additional features making it more powerful. + + - If someone asks, the extra feature is that the type on the left + of ":" can be arbitrary, like :rust:`Option`. + +- Note that Rust does not (yet) support specialization. For example, + given the original :rust:`duplicate`, it is invalid to add a specialized + :rust:`duplicate(a: u32)`. diff --git a/courses/comprehensive_rust_training/100_generics/05_impl_trait.rst b/courses/comprehensive_rust_training/100_generics/05_impl_trait.rst new file mode 100644 index 000000000..ad19c6c54 --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/05_impl_trait.rst @@ -0,0 +1,57 @@ +================ +"impl Trait" +================ + +---------------- +"impl Trait" +---------------- + +Similar to trait bounds, an :rust:`impl Trait` syntax can be used in +function arguments and return values: + +.. code:: rust + + // Syntactic sugar for: + // fn add_42_millions>(x: T) -> i32 { + fn add_42_millions(x: impl Into) -> i32 { + x.into() + 42_000_000 + } + + fn pair_of(x: u32) -> impl std::fmt::Debug { + (x + 1, x - 1) + } + + fn main() { + let many = add_42_millions(42_i8); + println!("{many}"); + let many_more = add_42_millions(10_000_000); + println!("{many_more}"); + let debuggable = pair_of(27); + println!("debuggable: {debuggable:?}"); + } + +--------- +Details +--------- + +:rust:`impl Trait` allows you to work with types which you cannot name. The +meaning of :rust:`impl Trait` is a bit different in the different positions. + +- For a parameter, :rust:`impl Trait` is like an anonymous generic + parameter with a trait bound. + +- For a return type, it means that the return type is some concrete + type that implements the trait, without naming the type. This can be + useful when you don't want to expose the concrete type in a public + API. + + Inference is hard in return position. A function returning + :rust:`impl Foo` picks the concrete type it returns, without writing it + out in the source. A function returning a generic type like + :rust:`collect() -> B` can return any type satisfying :rust:`B`, and the + caller may need to choose one, such as with + :rust:`let x: Vec<_> = foo.collect()` or with the turbofish, + :rust:`foo.collect::>()`. + +What is the type of :rust:`debuggable`? Try :rust:`let debuggable: () = ..` to +see what the error message shows. diff --git a/courses/comprehensive_rust_training/100_generics/06_dyn_trait.rst b/courses/comprehensive_rust_training/100_generics/06_dyn_trait.rst new file mode 100644 index 000000000..3b4e75101 --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/06_dyn_trait.rst @@ -0,0 +1,90 @@ +=============== +"dyn Trait" +=============== + +--------------- +"dyn Trait" +--------------- + +In addition to using traits for static dispatch via generics, Rust also +supports using them for type-erased, dynamic dispatch via trait objects: + +.. code:: rust + + struct Dog { + name: String, + age: i8, + } + struct Cat { + lives: i8, + } + + trait Pet { + fn talk(&self) -> String; + } + + impl Pet for Dog { + fn talk(&self) -> String { + format!("Woof, my name is {}!", self.name) + } + } + + impl Pet for Cat { + fn talk(&self) -> String { + String::from("Miau!") + } + } + + // Uses generics and static dispatch. + fn generic(pet: &impl Pet) { + println!("Hello, who are you? {}", pet.talk()); + } + + // Uses type-erasure and dynamic dispatch. + fn dynamic(pet: &dyn Pet) { + println!("Hello, who are you? {}", pet.talk()); + } + + fn main() { + let cat = Cat { lives: 9 }; + let dog = Dog { name: String::from("Fido"), age: 5 }; + + generic(&cat); + generic(&dog); + + dynamic(&cat); + dynamic(&dog); + } + +--------- +Details +--------- + +- Generics, including :rust:`impl Trait`, use monomorphization to create a + specialized instance of the function for each different type that the + generic is instantiated with. This means that calling a trait method + from within a generic function still uses static dispatch, as the + compiler has full type information and can resolve which type's trait + implementation to use. + +- When using :rust:`dyn Trait`, it instead uses dynamic dispatch through a + `virtual method table `__ + (vtable). This means that there's a single version of :rust:`fn dynamic` + that is used regardless of what type of :rust:`Pet` is passed in. + +- When using :rust:`dyn Trait`, the trait object needs to be behind some + kind of indirection. In this case it's a reference, though smart + pointer types like :rust:`Box` can also be used (this will be + demonstrated on day 3). + +- At runtime, a :rust:`&dyn Pet` is represented as a "fat pointer", i.e. a + pair of two pointers: One pointer points to the concrete object that + implements :rust:`Pet`, and the other points to the vtable for the trait + implementation for that type. When calling the :rust:`talk` method on + :rust:`&dyn Pet` the compiler looks up the function pointer for :rust:`talk` + in the vtable and then invokes the function, passing the pointer to + the :rust:`Dog` or :rust:`Cat` into that function. The compiler doesn't need + to know the concrete type of the :rust:`Pet` in order to do this. + +- A :rust:`dyn Trait` is considered to be :dfn:`type-erased`, because we no + longer have compile-time knowledge of what the concrete type is. diff --git a/courses/comprehensive_rust_training/100_generics/07_exercise.rst b/courses/comprehensive_rust_training/100_generics/07_exercise.rst new file mode 100644 index 000000000..4febe5162 --- /dev/null +++ b/courses/comprehensive_rust_training/100_generics/07_exercise.rst @@ -0,0 +1,30 @@ +=========================== +Exercise: Generic "min" +=========================== + +--------------------------- +Exercise: Generic "min" +--------------------------- + +In this short exercise, you will implement a generic :rust:`min` function +that determines the minimum of two values, using the +`Ord `__ +trait. + +.. code:: rust + + use std::cmp::Ordering; + + // TODO: implement the `min` function used in `main`. + + {{#include exercise.rs:main}} + +--------- +Details +--------- + +- Show students the + `Ord `__ + trait and + `Ordering `__ + enum. diff --git a/courses/comprehensive_rust_training/110_std_types.rst b/courses/comprehensive_rust_training/110_std_types.rst new file mode 100644 index 000000000..56e4b1186 --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types.rst @@ -0,0 +1,46 @@ +*********** +Std Types +*********** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 110_std_types/01_std.rst +.. include:: 110_std_types/02_docs.rst +.. include:: 110_std_types/03_option.rst +.. include:: 110_std_types/04_result.rst +.. include:: 110_std_types/05_string.rst +.. include:: 110_std_types/06_vec.rst +.. include:: 110_std_types/07_hashmap.rst +.. include:: 110_std_types/08_exercise.rst diff --git a/courses/comprehensive_rust_training/110_std_types/01_std.rst b/courses/comprehensive_rust_training/110_std_types/01_std.rst new file mode 100644 index 000000000..5901ea3ae --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/01_std.rst @@ -0,0 +1,22 @@ +================== +Standard Library +================== + +------------------ +Standard Library +------------------ + +Rust comes with a standard library which helps establish a set of common +types used by Rust libraries and programs. This way, two libraries can +work together smoothly because they both use the same :rust:`String` type. + +In fact, Rust contains several layers of the Standard Library: :rust:`core`, +:rust:`alloc` and :rust:`std`. + +- :rust:`core` includes the most basic types and functions that don't + depend on :rust:`libc`, allocator or even the presence of an operating + system. +- :rust:`alloc` includes types which require a global heap allocator, such + as :rust:`Vec`, :rust:`Box` and :rust:`Arc`. +- Embedded Rust applications often only use :rust:`core`, and sometimes + :rust:`alloc`. diff --git a/courses/comprehensive_rust_training/110_std_types/02_docs.rst b/courses/comprehensive_rust_training/110_std_types/02_docs.rst new file mode 100644 index 000000000..1fdc59fcb --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/02_docs.rst @@ -0,0 +1,54 @@ +=============== +Documentation +=============== + +--------------- +Documentation +--------------- + +Rust comes with extensive documentation. For example: + +- All of the details about + `loops `__. +- Primitive types like + `u8 `__. +- Standard library types like + `Option `__ + or + `BinaryHeap `__. + +Use :command:`rustup doc --std` or https://std.rs to view the documentation. + +In fact, you can document your own code: + +.. code:: rust + + /// Determine whether the first argument is divisible by the second argument. + /// + /// If the second argument is zero, the result is false. + fn is_divisible_by(lhs: u32, rhs: u32) -> bool { + if rhs == 0 { + return false; + } + lhs % rhs == 0 + } + +The contents are treated as Markdown. All published Rust library crates +are automatically documented at `docs.rs `__ using +the `rustdoc `__ +tool. It is idiomatic to document all public items in an API using this +pattern. + +To document an item from inside the item (such as inside a module), use +:rust:`//!` or :rust:`/*! .. */`, called :dfn:`inner doc comments`: + +.. code:: rust + + //! This module contains functionality relating to divisibility of integers. + +--------- +Details +--------- + +- Show students the generated docs for the :rust:`rand` crate at + https://docs.rs/rand. diff --git a/courses/comprehensive_rust_training/110_std_types/03_option.rst b/courses/comprehensive_rust_training/110_std_types/03_option.rst new file mode 100644 index 000000000..a8e584b17 --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/03_option.rst @@ -0,0 +1,44 @@ +======== +Option +======== + +-------- +Option +-------- + +We have already seen some use of :rust:`Option`. It stores either a value +of type :rust:`T` or nothing. For example, +`String::find `__ +returns an :rust:`Option`. + +.. code:: rust + + fn main() { + let name = "Alexander the Great"; + let mut position: Option = name.find('e'); + println!("find returned {position:?}"); + assert_eq!(position.unwrap(), 14); + position = name.find('Z'); + println!("find returned {position:?}"); + assert_eq!(position.expect("Character not found"), 0); + } + +--------- +Details +--------- + +- :rust:`Option` is widely used, not just in the standard library. +- :rust:`unwrap` will return the value in an :rust:`Option`, or panic. + :rust:`expect` is similar but takes an error message. + + - You can panic on None, but you can't "accidentally" forget to + check for None. + - It's common to :rust:`unwrap`/:rust:`expect` all over the place when + hacking something together, but production code typically handles + :rust:`None` in a nicer fashion. + +- The "niche optimization" means that :rust:`Option` often has the same + size in memory as :rust:`T`, if there is some representation that is not + a valid value of T. For example, a reference cannot be NULL, so + :rust:`Option<&T>` automatically uses NULL to represent the :rust:`None` + variant, and thus can be stored in the same memory as :rust:`&T`. diff --git a/courses/comprehensive_rust_training/110_std_types/04_result.rst b/courses/comprehensive_rust_training/110_std_types/04_result.rst new file mode 100644 index 000000000..4046cd4fc --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/04_result.rst @@ -0,0 +1,49 @@ +======== +Result +======== + +-------- +Result +-------- + +:rust:`Result` is similar to :rust:`Option`, but indicates the success or +failure of an operation, each with a different enum variant. It is +generic: :rust:`Result` where :rust:`T` is used in the :rust:`Ok` variant and +:rust:`E` appears in the :rust:`Err` variant. + +.. code:: rust + + use std::fs::File; + use std::io::Read; + + fn main() { + let file: Result = File::open("diary.txt"); + match file { + Ok(mut file) => { + let mut contents = String::new(); + if let Ok(bytes) = file.read_to_string(&mut contents) { + println!("Dear diary: {contents} ({bytes} bytes)"); + } else { + println!("Could not read file content"); + } + } + Err(err) => { + println!("The diary could not be opened: {err}"); + } + } + } + +--------- +Details +--------- + +- As with :rust:`Option`, the successful value sits inside of :rust:`Result`, + forcing the developer to explicitly extract it. This encourages error + checking. In the case where an error should never happen, + :rust:`unwrap()` or :rust:`expect()` can be called, and this is a signal of + the developer intent too. +- :rust:`Result` documentation is a recommended read. Not during the + course, but it is worth mentioning. It contains a lot of convenience + methods and functions that help functional-style programming. +- :rust:`Result` is the standard type to implement error handling as we + will see on Day 4. diff --git a/courses/comprehensive_rust_training/110_std_types/05_string.rst b/courses/comprehensive_rust_training/110_std_types/05_string.rst new file mode 100644 index 000000000..1e5177777 --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/05_string.rst @@ -0,0 +1,71 @@ +======== +String +======== + +-------- +String +-------- + +`String `__ +is a growable UTF-8 encoded string: + +.. code:: rust + + fn main() { + let mut s1 = String::new(); + s1.push_str("Hello"); + println!("s1: len = {}, capacity = {}", s1.len(), s1.capacity()); + + let mut s2 = String::with_capacity(s1.len() + 1); + s2.push_str(&s1); + s2.push('!'); + println!("s2: len = {}, capacity = {}", s2.len(), s2.capacity()); + + let s3 = String::from("a z"); + println!("s3: len = {}, number of chars = {}", s3.len(), s3.chars().count()); + } + +:rust:`String` implements +`Deref `__, +which means that you can call all :rust:`str` methods on a :rust:`String`. + +--------- +Details +--------- + +- :rust:`String::new` returns a new empty string, use + :rust:`String::with_capacity` when you know how much data you want to + push to the string. +- :rust:`String::len` returns the size of the :rust:`String` in bytes (which + can be different from its length in characters). +- :rust:`String::chars` returns an iterator over the actual characters. + Note that a :rust:`char` can be different from what a human will consider + a "character" due to + `grapheme clusters `__. +- When people refer to strings they could either be talking about + :rust:`&str` or :rust:`String`. +- When a type implements :rust:`Deref`, the compiler will let + you transparently call methods from :rust:`T`. + + - We haven't discussed the :rust:`Deref` trait yet, so at this point + this mostly explains the structure of the sidebar in the + documentation. + - :rust:`String` implements :rust:`Deref` which transparently + gives it access to :rust:`str` methods. + - Write and compare :rust:`let s3 = s1.deref();` and :rust:`let s3 = &*s1;`. + +- :rust:`String` is implemented as a wrapper around a vector of bytes, many + of the operations you see supported on vectors are also supported on + :rust:`String`, but with some extra guarantees. +- Compare the different ways to index a :rust:`String`: + + - To a character by using :rust:`s3.chars().nth(i).unwrap()` where :rust:`i` + is in-bound, out-of-bounds. + - To a substring by using :rust:`s3[0..4]`, where that slice is on + character boundaries or not. + +- Many types can be converted to a string with the + `to_string `__ + method. This trait is automatically implemented for all types that + implement :rust:`Display`, so anything that can be formatted can also be + converted to a string. diff --git a/courses/comprehensive_rust_training/110_std_types/06_vec.rst b/courses/comprehensive_rust_training/110_std_types/06_vec.rst new file mode 100644 index 000000000..53423bbfd --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/06_vec.rst @@ -0,0 +1,58 @@ +========= +"Vec" +========= + +--------- +"Vec" +--------- + +`Vec `__ is the +standard resizable heap-allocated buffer: + +.. code:: rust + + fn main() { + let mut v1 = Vec::new(); + v1.push(42); + println!("v1: len = {}, capacity = {}", v1.len(), v1.capacity()); + + let mut v2 = Vec::with_capacity(v1.len() + 1); + v2.extend(v1.iter()); + v2.push(9999); + println!("v2: len = {}, capacity = {}", v2.len(), v2.capacity()); + + // Canonical macro to initialize a vector with elements. + let mut v3 = vec![0, 0, 1, 2, 3, 4]; + + // Retain only the even elements. + v3.retain(|x| x % 2 == 0); + println!("{v3:?}"); + + // Remove consecutive duplicates. + v3.dedup(); + println!("{v3:?}"); + } + +:rust:`Vec` implements +`Deref `__, +which means that you can call slice methods on a :rust:`Vec`. + +--------- +Details +--------- + +- :rust:`Vec` is a type of collection, along with :rust:`String` and + :rust:`HashMap`. The data it contains is stored on the heap. This means + the amount of data doesn't need to be known at compile time. It can + grow or shrink at runtime. +- Notice how :rust:`Vec` is a generic type too, but you don't have to + specify :rust:`T` explicitly. As always with Rust type inference, the + :rust:`T` was established during the first :rust:`push` call. +- :rust:`vec![...]` is a canonical macro to use instead of :rust:`Vec::new()` + and it supports adding initial elements to the vector. +- To index the vector you use :rust:`[` :rust:`]`, but they will panic if out + of bounds. Alternatively, using :rust:`get` will return an :rust:`Option`. + The :rust:`pop` function will remove the last element. +- Slices are covered on day 3. For now, students only need to know that + a value of type :rust:`Vec` gives access to all of the documented slice + methods, too. diff --git a/courses/comprehensive_rust_training/110_std_types/07_hashmap.rst b/courses/comprehensive_rust_training/110_std_types/07_hashmap.rst new file mode 100644 index 000000000..3cf77dba9 --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/07_hashmap.rst @@ -0,0 +1,86 @@ +============= +"HashMap" +============= + +------------- +"HashMap" +------------- + +Standard hash map with protection against HashDoS attacks: + +.. code:: rust + + use std::collections::HashMap; + + fn main() { + let mut page_counts = HashMap::new(); + page_counts.insert("Adventures of Huckleberry Finn", 207); + page_counts.insert("Grimms' Fairy Tales", 751); + page_counts.insert("Pride and Prejudice", 303); + + if !page_counts.contains_key("Les Miserables") { + println!( + "We know about {} books, but not Les Miserables.", + page_counts.len() + ); + } + + for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] { + match page_counts.get(book) { + Some(count) => println!("{book}: {count} pages"), + None => println!("{book} is unknown."), + } + } + + // Use the .entry() method to insert a value if nothing is found. + for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] { + let page_count: &mut i32 = page_counts.entry(book).or_insert(0); + *page_count += 1; + } + + println!("{page_counts:#?}"); + } + +--------- +Details +--------- + +- :rust:`HashMap` is not defined in the prelude and needs to be brought + into scope. + +- Try the following lines of code. The first line will see if a book is + in the hashmap and if not return an alternative value. The second + line will insert the alternative value in the hashmap if the book is + not found. + + .. code:: rust + + let pc1 = page_counts + .get("Harry Potter and the Sorcerer's Stone") + .unwrap_or(&336); + let pc2 = page_counts + .entry("The Hunger Games") + .or_insert(374); + +- Unlike :rust:`vec!`, there is unfortunately no standard :rust:`hashmap!` + macro. + + - Although, since Rust 1.56, HashMap implements + `From<[(K, V); N]> `__, + which allows us to easily initialize a hash map from a literal + array: + + .. code:: rust + + let page_counts = HashMap::from([ + ("Harry Potter and the Sorcerer's Stone".to_string(), 336), + ("The Hunger Games".to_string(), 374), + ]); + +- Alternatively HashMap can be built from any :rust:`Iterator` which yields + key-value tuples. + +- This type has several "method-specific" return types, such as + :rust:`std::collections::hash_map::Keys`. These types often appear in + searches of the Rust docs. Show students the docs for this type, and + the helpful link back to the :rust:`keys` method. diff --git a/courses/comprehensive_rust_training/110_std_types/08_exercise.rst b/courses/comprehensive_rust_training/110_std_types/08_exercise.rst new file mode 100644 index 000000000..0539903d2 --- /dev/null +++ b/courses/comprehensive_rust_training/110_std_types/08_exercise.rst @@ -0,0 +1,56 @@ +=================== +Exercise: Counter +=================== + +------------------- +Exercise: Counter +------------------- + +In this exercise you will take a very simple data structure and make it +generic. It uses a +`std::collections::HashMap `__ +to keep track of which values have been seen and how many times each one +has appeared. + +The initial version of :rust:`Counter` is hard coded to only work for +:rust:`u32` values. Make the struct and its methods generic over the type of +value being tracked, that way :rust:`Counter` can track any type of value. + +If you finish early, try using the +`entry `__ +method to halve the number of hash lookups required to implement the +:rust:`count` method. + +.. code:: rust + + use std::collections::HashMap; + + /// Counter counts the number of times each value of type T has been seen. + struct Counter { + values: HashMap, + } + + impl Counter { + /// Create a new Counter. + fn new() -> Self { + Counter { + values: HashMap::new(), + } + } + + /// Count an occurrence of the given value. + fn count(&mut self, value: u32) { + if self.values.contains_key(&value) { + *self.values.get_mut(&value).unwrap() += 1; + } else { + self.values.insert(value, 1); + } + } + + /// Return the number of times the given value has been seen. + fn times_seen(&self, value: u32) -> u64 { + self.values.get(&value).copied().unwrap_or_default() + } + } + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/120_std_traits.rst b/courses/comprehensive_rust_training/120_std_traits.rst new file mode 100644 index 000000000..7025ac99a --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits.rst @@ -0,0 +1,46 @@ +************ +Std Traits +************ + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 120_std_traits/01_comparisons.rst +.. include:: 120_std_traits/02_operators.rst +.. include:: 120_std_traits/03_from_and_into.rst +.. include:: 120_std_traits/04_casting.rst +.. include:: 120_std_traits/05_read_and_write.rst +.. include:: 120_std_traits/06_default.rst +.. include:: 120_std_traits/07_closures.rst +.. include:: 120_std_traits/08_exercise.rst diff --git a/courses/comprehensive_rust_training/120_std_traits/01_comparisons.rst b/courses/comprehensive_rust_training/120_std_traits/01_comparisons.rst new file mode 100644 index 000000000..0354ac9c0 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/01_comparisons.rst @@ -0,0 +1,83 @@ +============= +Comparisons +============= + +------------- +Comparisons +------------- + +These traits support comparisons between values. All traits can be +derived for types containing fields that implement these traits. + +-------------------------- +"PartialEq" and "Eq" +-------------------------- + +:rust:`PartialEq` is a partial equivalence relation, with required method +:rust:`eq` and provided method :rust:`ne`. The :rust:`==` and :rust:`!=` operators will +call these methods. + +.. code:: rust + + struct Key { + id: u32, + metadata: Option, + } + impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } + } + +:rust:`Eq` is a full equivalence relation (reflexive, symmetric, and +transitive) and implies :rust:`PartialEq`. Functions that require full +equivalence will use :rust:`Eq` as a trait bound. + +---------------------------- +"PartialOrd" and "Ord" +---------------------------- + +:rust:`PartialOrd` defines a partial ordering, with a :rust:`partial_cmp` +method. It is used to implement the :rust:`<`, :rust:`<=`, :rust:`>=`, and :rust:`>` +operators. + +.. code:: rust + + use std::cmp::Ordering; + #[derive(Eq, PartialEq)] + struct Citation { + author: String, + year: u32, + } + impl PartialOrd for Citation { + fn partial_cmp(&self, other: &Self) -> Option { + match self.author.partial_cmp(&other.author) { + Some(Ordering::Equal) => self.year.partial_cmp(&other.year), + author_ord => author_ord, + } + } + } + +:rust:`Ord` is a total ordering, with :rust:`cmp` returning :rust:`Ordering`. + +--------- +Details +--------- + +:rust:`PartialEq` can be implemented between different types, but :rust:`Eq` +cannot, because it is reflexive: + +.. code:: rust + + struct Key { + id: u32, + metadata: Option, + } + impl PartialEq for Key { + fn eq(&self, other: &u32) -> bool { + self.id == *other + } + } + +In practice, it's common to derive these traits, but uncommon to +implement them. diff --git a/courses/comprehensive_rust_training/120_std_traits/02_operators.rst b/courses/comprehensive_rust_training/120_std_traits/02_operators.rst new file mode 100644 index 000000000..53c974308 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/02_operators.rst @@ -0,0 +1,61 @@ +=========== +Operators +=========== + +----------- +Operators +----------- + +Operator overloading is implemented via traits in +`std::ops `__: + +.. code:: rust + + #[derive(Debug, Copy, Clone)] + struct Point { + x: i32, + y: i32, + } + + impl std::ops::Add for Point { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { x: self.x + other.x, y: self.y + other.y } + } + } + + fn main() { + let p1 = Point { x: 10, y: 20 }; + let p2 = Point { x: 100, y: 200 }; + println!("{p1:?} + {p2:?} = {:?}", p1 + p2); + } + +--------- +Details +--------- + +Discussion points: + +- You could implement :rust:`Add` for :rust:`&Point`. In which situations is + that useful? + + - Answer: :rust:`Add:add` consumes :rust:`self`. If type :rust:`T` for which you + are overloading the operator is not :rust:`Copy`, you should consider + overloading the operator for :rust:`&T` as well. This avoids + unnecessary cloning on the call site. + +- Why is :rust:`Output` an associated type? Could it be made a type + parameter of the method? + + - Short answer: Function type parameters are controlled by the + caller, but associated types (like :rust:`Output`) are controlled by + the implementer of a trait. + +- You could implement :rust:`Add` for two different types, e.g. + :rust:`impl Add<(i32, i32)> for Point` would add a tuple to a :rust:`Point`. + +The :rust:`Not` trait (:rust:`!` operator) is notable because it does not +"boolify" like the same operator in C-family languages; instead, for +integer types it negates each bit of the number, which arithmetically is +equivalent to subtracting it from -1: :rust:`!5 == -6`. diff --git a/courses/comprehensive_rust_training/120_std_traits/03_from_and_into.rst b/courses/comprehensive_rust_training/120_std_traits/03_from_and_into.rst new file mode 100644 index 000000000..f11728265 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/03_from_and_into.rst @@ -0,0 +1,49 @@ +======================= +"From" and "Into" +======================= + +----------------------- +"From" and "Into" +----------------------- + +Types implement +`From `__ and +`Into `__ to +facilitate type conversions. Unlike :rust:`as`, these traits correspond to +lossless, infallible conversions. + +.. code:: rust + + fn main() { + let s = String::from("hello"); + let addr = std::net::Ipv4Addr::from([127, 0, 0, 1]); + let one = i16::from(true); + let bigger = i32::from(123_i16); + println!("{s}, {addr}, {one}, {bigger}"); + } + +`Into `__ is +automatically implemented when +`From `__ is +implemented: + +.. code:: rust + + fn main() { + let s: String = "hello".into(); + let addr: std::net::Ipv4Addr = [127, 0, 0, 1].into(); + let one: i16 = true.into(); + let bigger: i32 = 123_i16.into(); + println!("{s}, {addr}, {one}, {bigger}"); + } + +--------- +Details +--------- + +- That's why it is common to only implement :rust:`From`, as your type will + get :rust:`Into` implementation too. +- When declaring a function argument input type like "anything that can + be converted into a :rust:`String`", the rule is opposite, you should use + :rust:`Into`. Your function will accept types that implement :rust:`From` and + those that *only* implement :rust:`Into`. diff --git a/courses/comprehensive_rust_training/120_std_traits/04_casting.rst b/courses/comprehensive_rust_training/120_std_traits/04_casting.rst new file mode 100644 index 000000000..aaa76e273 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/04_casting.rst @@ -0,0 +1,47 @@ +========= +Casting +========= + +--------- +Casting +--------- + +Rust has no *implicit* type conversions, but does support explicit casts +with :rust:`as`. These generally follow C semantics where those are defined. + +.. code:: rust + + fn main() { + let value: i64 = 1000; + println!("as u16: {}", value as u16); + println!("as i16: {}", value as i16); + println!("as u8: {}", value as u8); + } + +The results of :rust:`as` are *always* defined in Rust and consistent across +platforms. This might not match your intuition for changing sign or +casting to a smaller type - check the docs, and comment for clarity. + +Casting with :rust:`as` is a relatively sharp tool that is easy to use +incorrectly, and can be a source of subtle bugs as future maintenance +work changes the types that are used or the ranges of values in types. +Casts are best used only when the intent is to indicate unconditional +truncation (e.g. selecting the bottom 32 bits of a :rust:`u64` with +:rust:`as u32`, regardless of what was in the high bits). + +For infallible casts (e.g. :rust:`u32` to :rust:`u64`), prefer using :rust:`From` or +:rust:`Into` over :rust:`as` to confirm that the cast is in fact infallible. For +fallible casts, :rust:`TryFrom` and :rust:`TryInto` are available when you want +to handle casts that fit differently from those that don't. + +--------- +Details +--------- + +Consider taking a break after this slide. + +:rust:`as` is similar to a C++ static cast. Use of :rust:`as` in cases where +data might be lost is generally discouraged, or at least deserves an +explanatory comment. + +This is common in casting integers to :rust:`usize` for use as an index. diff --git a/courses/comprehensive_rust_training/120_std_traits/05_read_and_write.rst b/courses/comprehensive_rust_training/120_std_traits/05_read_and_write.rst new file mode 100644 index 000000000..c39d2b4ca --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/05_read_and_write.rst @@ -0,0 +1,51 @@ +======================== +"Read" and "Write" +======================== + +------------------------ +"Read" and "Write" +------------------------ + +Using `Read `__ +and +`BufRead `__, +you can abstract over :rust:`u8` sources: + +.. code:: rust + + use std::io::{BufRead, BufReader, Read, Result}; + + fn count_lines(reader: R) -> usize { + let buf_reader = BufReader::new(reader); + buf_reader.lines().count() + } + + fn main() -> Result<()> { + let slice: &[u8] = b"foo\nbar\nbaz\n"; + println!("lines in slice: {}", count_lines(slice)); + + let file = std::fs::File::open(std::env::current_exe()?)?; + println!("lines in file: {}", count_lines(file)); + Ok(()) + } + +Similarly, +`Write `__ lets +you abstract over :rust:`u8` sinks: + +.. code:: rust + + use std::io::{Result, Write}; + + fn log(writer: &mut W, msg: &str) -> Result<()> { + writer.write_all(msg.as_bytes())?; + writer.write_all("\n".as_bytes()) + } + + fn main() -> Result<()> { + let mut buffer = Vec::new(); + log(&mut buffer, "Hello")?; + log(&mut buffer, "World")?; + println!("Logged: {buffer:?}"); + Ok(()) + } diff --git a/courses/comprehensive_rust_training/120_std_traits/06_default.rst b/courses/comprehensive_rust_training/120_std_traits/06_default.rst new file mode 100644 index 000000000..520647705 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/06_default.rst @@ -0,0 +1,59 @@ +======================= +The "Default" Trait +======================= + +----------------------- +The "Default" Trait +----------------------- + +`Default `__ +trait produces a default value for a type. + +.. code:: rust + + #[derive(Debug, Default)] + struct Derived { + x: u32, + y: String, + z: Implemented, + } + + #[derive(Debug)] + struct Implemented(String); + + impl Default for Implemented { + fn default() -> Self { + Self("John Smith".into()) + } + } + + fn main() { + let default_struct = Derived::default(); + println!("{default_struct:#?}"); + + let almost_default_struct = + Derived { y: "Y is set!".into(), ..Derived::default() }; + println!("{almost_default_struct:#?}"); + + let nothing: Option = None; + println!("{:#?}", nothing.unwrap_or_default()); + } + +--------- +Details +--------- + +- It can be implemented directly or it can be derived via + :rust:`#[derive(Default)]`. +- A derived implementation will produce a value where all fields are + set to their default values. + + - This means all types in the struct must implement :rust:`Default` too. + +- Standard Rust types often implement :rust:`Default` with reasonable + values (e.g. :rust:`0`, :rust:`""`, etc). +- The partial struct initialization works nicely with default. +- The Rust standard library is aware that types can implement + :rust:`Default` and provides convenience methods that use it. +- The :rust:`..` syntax is called + `struct update syntax `__. diff --git a/courses/comprehensive_rust_training/120_std_traits/07_closures.rst b/courses/comprehensive_rust_training/120_std_traits/07_closures.rst new file mode 100644 index 000000000..8e77d95fb --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/07_closures.rst @@ -0,0 +1,87 @@ +========== +Closures +========== + +---------- +Closures +---------- + +Closures or lambda expressions have types which cannot be named. +However, they implement special +`Fn `__, +`FnMut `__, and +`FnOnce `__ +traits: + +.. code:: rust + + fn apply_and_log(func: impl FnOnce(i32) -> i32, func_name: &str, input: i32) { + println!("Calling {func_name}({input}): {}", func(input)) + } + + fn main() { + let n = 3; + let add_3 = |x| x + n; + apply_and_log(&add_3, "add_3", 10); + apply_and_log(&add_3, "add_3", 20); + + let mut v = Vec::new(); + let mut accumulate = |x: i32| { + v.push(x); + v.iter().sum::() + }; + apply_and_log(&mut accumulate, "accumulate", 4); + apply_and_log(&mut accumulate, "accumulate", 5); + + let multiply_sum = |x| x * v.into_iter().sum::(); + apply_and_log(multiply_sum, "multiply_sum", 3); + } + +--------- +Details +--------- + +An :rust:`Fn` (e.g. :rust:`add_3`) neither consumes nor mutates captured values. +It can be called needing only a shared reference to the closure, which +means the closure can be executed repeatedly and even concurrently. + +An :rust:`FnMut` (e.g. :rust:`accumulate`) might mutate captured values. The +closure object is accessed via exclusive reference, so it can be called +repeatedly but not concurrently. + +If you have an :rust:`FnOnce` (e.g. :rust:`multiply_sum`), you may only call it +once. Doing so consumes the closure and any values captured by move. + +:rust:`FnMut` is a subtype of :rust:`FnOnce`. :rust:`Fn` is a subtype of :rust:`FnMut` +and :rust:`FnOnce`. I.e. you can use an :rust:`FnMut` wherever an :rust:`FnOnce` is +called for, and you can use an :rust:`Fn` wherever an :rust:`FnMut` or +:rust:`FnOnce` is called for. + +When you define a function that takes a closure, you should take +:rust:`FnOnce` if you can (i.e. you call it once), or :rust:`FnMut` else, and +last :rust:`Fn`. This allows the most flexibility for the caller. + +In contrast, when you have a closure, the most flexible you can have is +:rust:`Fn` (which can be passed to a consumer of any of the 3 closure +traits), then :rust:`FnMut`, and lastly :rust:`FnOnce`. + +The compiler also infers :rust:`Copy` (e.g. for :rust:`add_3`) and :rust:`Clone` +(e.g. :rust:`multiply_sum`), depending on what the closure captures. +Function pointers (references to :rust:`fn` items) implement :rust:`Copy` and +:rust:`Fn`. + +By default, closures will capture each variable from an outer scope by +the least demanding form of access they can (by shared reference if +possible, then exclusive reference, then by move). The :rust:`move` keyword +forces capture by value. + +.. code:: rust + + fn make_greeter(prefix: String) -> impl Fn(&str) { + return move |name| println!("{} {}", prefix, name); + } + + fn main() { + let hi = make_greeter("Hi".to_string()); + hi("Greg"); + } diff --git a/courses/comprehensive_rust_training/120_std_traits/08_exercise.rst b/courses/comprehensive_rust_training/120_std_traits/08_exercise.rst new file mode 100644 index 000000000..f1a432cd0 --- /dev/null +++ b/courses/comprehensive_rust_training/120_std_traits/08_exercise.rst @@ -0,0 +1,23 @@ +================= +Exercise: ROT13 +================= + +----------------- +Exercise: ROT13 +----------------- + +In this example, you will implement the classic +`"ROT13" cipher `__. Copy this code to the +playground, and implement the missing bits. Only rotate ASCII alphabetic +characters, to ensure the result is still valid UTF-8. + +.. code:: rust + + {{#include exercise.rs:head }} + + // Implement the `Read` trait for `RotDecoder`. + + {{#include exercise.rs:main }} + +What happens if you chain two :rust:`RotDecoder` instances together, each +rotating by 13 characters? diff --git a/courses/comprehensive_rust_training/130_memory_management.rst b/courses/comprehensive_rust_training/130_memory_management.rst new file mode 100644 index 000000000..328a173c9 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management.rst @@ -0,0 +1,46 @@ +******************* +Memory Management +******************* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 130_memory_management/01_review.rst +.. include:: 130_memory_management/02_approaches.rst +.. include:: 130_memory_management/03_ownership.rst +.. include:: 130_memory_management/04_move.rst +.. include:: 130_memory_management/05_clone.rst +.. include:: 130_memory_management/06_copy_types.rst +.. include:: 130_memory_management/07_drop.rst +.. include:: 130_memory_management/08_exercise.rst diff --git a/courses/comprehensive_rust_training/130_memory_management/01_review.rst b/courses/comprehensive_rust_training/130_memory_management/01_review.rst new file mode 100644 index 000000000..d8928edb0 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/01_review.rst @@ -0,0 +1,85 @@ +========================== +Review of Program Memory +========================== + +-------------------------- +Review of Program Memory +-------------------------- + +Programs allocate memory in two ways: + +- Stack: Continuous area of memory for local variables. + + - Values have fixed sizes known at compile time. + - Extremely fast: just move a stack pointer. + - Easy to manage: follows function calls. + - Great memory locality. + +- Heap: Storage of values outside of function calls. + + - Values have dynamic sizes determined at runtime. + - Slightly slower than the stack: some book-keeping needed. + - No guarantee of memory locality. + +--------- +Example +--------- + +Creating a :rust:`String` puts fixed-sized metadata on the stack and +dynamically sized data, the actual string, on the heap: + +.. code:: rust + + fn main() { + let s1 = String::from("Hello"); + } + +.. code:: bob + + Stack + .- - - - - - - - - - - - - -. Heap + : : .- - - - - - - - - - - - - - - -. + : s1 : : : + : +-----------+-------+ : : : + : | capacity | 5 | : : +----+----+----+----+----+ : + : | ptr | o-+---+-----+-->| H | e | l | l | o | : + : | len | 5 | : : +----+----+----+----+----+ : + : +-----------+-------+ : : : + : : : : + `- - - - - - - - - - - - - -' `- - - - - - - - - - - - - - - -' + +--------- +Details +--------- + +- Mention that a :rust:`String` is backed by a :rust:`Vec`, so it has a + capacity and length and can grow if mutable via reallocation on the + heap. + +- If students ask about it, you can mention that the underlying memory + is heap allocated using the + `System Allocator `__ + and custom allocators can be implemented using the + `Allocator API `__ + +----------------- +More to Explore +----------------- + +We can inspect the memory layout with :rust:`unsafe` Rust. However, you +should point out that this is rightfully unsafe! + +.. code:: rust + + fn main() { + let mut s1 = String::from("Hello"); + s1.push(' '); + s1.push_str("world"); + // DON'T DO THIS AT HOME! For educational purposes only. + // String provides no guarantees about its layout, so this could lead to + // undefined behavior. + unsafe { + let (capacity, ptr, len): (usize, usize, usize) = std::mem::transmute(s1); + println!("capacity = {capacity}, ptr = {ptr:#x}, len = {len}"); + } + } diff --git a/courses/comprehensive_rust_training/130_memory_management/02_approaches.rst b/courses/comprehensive_rust_training/130_memory_management/02_approaches.rst new file mode 100644 index 000000000..b869d2330 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/02_approaches.rst @@ -0,0 +1,61 @@ +================================= +Approaches to Memory Management +================================= + +--------------------------------- +Approaches to Memory Management +--------------------------------- + +Traditionally, languages have fallen into two broad categories: + +- Full control via manual memory management: C, C++, Pascal, ... + + - Programmer decides when to allocate or free heap memory. + - Programmer must determine whether a pointer still points to valid + memory. + - Studies show, programmers make mistakes. + +- Full safety via automatic memory management at runtime: Java, Python, + Go, Haskell, ... + + - A runtime system ensures that memory is not freed until it can no + longer be referenced. + - Typically implemented with reference counting or garbage + collection. + +Rust offers a new mix: + + Full control *and* safety via compile time enforcement of correct + memory management. + +It does this with an explicit ownership concept. + +--------- +Details +--------- + +This slide is intended to help students coming from other languages to +put Rust in context. + +- C must manage heap manually with :cpp:`malloc` and :cpp:`free`. Common + errors include forgetting to call :cpp:`free`, calling it multiple times + for the same pointer, or dereferencing a pointer after the memory it + points to has been freed. + +- C++ has tools like smart pointers (:cpp:`unique_ptr`, :cpp:`shared_ptr`) + that take advantage of language guarantees about calling destructors + to ensure memory is freed when a function returns. It is still quite + easy to mis-use these tools and create similar bugs to C. + +- Java, Go, and Python rely on the garbage collector to identify memory + that is no longer reachable and discard it. This guarantees that any + pointer can be dereferenced, eliminating use-after-free and other + classes of bugs. But, GC has a runtime cost and is difficult to tune + properly. + +Rust's ownership and borrowing model can, in many cases, get the +performance of C, with alloc and free operations precisely where they +are required - zero cost. It also provides tools similar to C++'s smart +pointers. When required, other options such as reference counting are +available, and there are even crates available to support runtime +garbage collection (not covered in this class). diff --git a/courses/comprehensive_rust_training/130_memory_management/03_ownership.rst b/courses/comprehensive_rust_training/130_memory_management/03_ownership.rst new file mode 100644 index 000000000..976548aff --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/03_ownership.rst @@ -0,0 +1,36 @@ +=========== +Ownership +=========== + +----------- +Ownership +----------- + +All variable bindings have a *scope* where they are valid and it is an +error to use a variable outside its scope: + +.. code:: rust + + struct Point(i32, i32); + + fn main() { + { + let p = Point(3, 4); + println!("x: {}", p.0); + } + println!("y: {}", p.1); + } + +We say that the variable *owns* the value. Every Rust value has +precisely one owner at all times. + +At the end of the scope, the variable is *dropped* and the data is +freed. A destructor can run here to free up resources. + +--------- +Details +--------- + +Students familiar with garbage-collection implementations will know that +a garbage collector starts with a set of "roots" to find all reachable +memory. Rust's "single owner" principle is a similar idea. diff --git a/courses/comprehensive_rust_training/130_memory_management/04_move.rst b/courses/comprehensive_rust_training/130_memory_management/04_move.rst new file mode 100644 index 000000000..96bf8e785 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/04_move.rst @@ -0,0 +1,182 @@ +================ +Move Semantics +================ + +---------------- +Move Semantics +---------------- + +An assignment will transfer *ownership* between variables: + +.. code:: rust + + fn main() { + let s1: String = String::from("Hello!"); + let s2: String = s1; + println!("s2: {s2}"); + // println!("s1: {s1}"); + } + +- The assignment of :rust:`s1` to :rust:`s2` transfers ownership. +- When :rust:`s1` goes out of scope, nothing happens: it does not own + anything. +- When :rust:`s2` goes out of scope, the string data is freed. + +Before move to :rust:`s2`: + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - -. + : : : : + : s1 : : : + : +-----------+-------+ : : +----+----+----+----+----+----+ : + : | ptr | o---+---+-----+-->| H | e | l | l | o | ! | : + : | len | 6 | : : +----+----+----+----+----+----+ : + : | capacity | 6 | : : : + : +-----------+-------+ : : : + : : `- - - - - - - - - - - - - - - - - - -' + : : + `- - - - - - - - - - - - - -' + +After move to :rust:`s2`: + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - -. + : : : : + : s1 "(inaccessible)" : : : + : +-----------+-------+ : : +----+----+----+----+----+----+ : + : | ptr | o---+---+--+--+-->| H | e | l | l | o | ! | : + : | len | 6 | : | : +----+----+----+----+----+----+ : + : | capacity | 6 | : | : : + : +-----------+-------+ : | : : + : : | `- - - - - - - - - - - - - - - - - - -' + : s2 : | + : +-----------+-------+ : | + : | ptr | o---+---+--' + : | len | 6 | : + : | capacity | 6 | : + : +-----------+-------+ : + : : + `- - - - - - - - - - - - - -' + +When you pass a value to a function, the value is assigned to the +function parameter. This transfers ownership: + +.. code:: rust + + fn say_hello(name: String) { + println!("Hello {name}") + } + + fn main() { + let name = String::from("Alice"); + say_hello(name); + // say_hello(name); + } + +--------- +Details +--------- + +- Mention that this is the opposite of the defaults in C++, which + copies by value unless you use :cpp:`std::move` (and the move + constructor is defined!). + +- It is only the ownership that moves. Whether any machine code is + generated to manipulate the data itself is a matter of optimization, + and such copies are aggressively optimized away. + +- Simple values (such as integers) can be marked :rust:`Copy` (see later + slides). + +- In Rust, clones are explicit (by using :rust:`clone`). + +In the :rust:`say_hello` example: + +- With the first call to :rust:`say_hello`, :rust:`main` gives up ownership of + :rust:`name`. Afterwards, :rust:`name` cannot be used anymore within + :rust:`main`. +- The heap memory allocated for :rust:`name` will be freed at the end of + the :rust:`say_hello` function. +- :rust:`main` can retain ownership if it passes :rust:`name` as a reference + (:rust:`&name`) and if :rust:`say_hello` accepts a reference as a parameter. +- Alternatively, :rust:`main` can pass a clone of :rust:`name` in the first + call (:rust:`name.clone()`). +- Rust makes it harder than C++ to inadvertently create copies by + making move semantics the default, and by forcing programmers to make + clones explicit. + +-------------------------------- +Defensive Copies in Modern C++ +-------------------------------- + +Modern C++ solves this differently: + +.. code:: cpp + + std::string s1 = "Cpp"; + std::string s2 = s1; // Duplicate the data in s1. + +- The heap data from :rust:`s1` is duplicated and :rust:`s2` gets its own + independent copy. +- When :rust:`s1` and :rust:`s2` go out of scope, they each free their own + memory. + +Before copy-assignment: + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - -. .- - - - - - - - - - - -. + : : : : + : s1 : : : + : +-----------+-------+ : : +----+----+----+ : + : | ptr | o---+---+--+--+-->| C | p | p | : + : | len | 3 | : : +----+----+----+ : + : | capacity | 3 | : : : + : +-----------+-------+ : : : + : : `- - - - - - - - - - - -' + `- - - - - - - - - - - - - -' + +After copy-assignment: + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - -. .- - - - - - - - - - - -. + : : : : + : s1 : : : + : +-----------+-------+ : : +----+----+----+ : + : | ptr | o---+---+--+--+-->| C | p | p | : + : | len | 3 | : : +----+----+----+ : + : | capacity | 3 | : : : + : +-----------+-------+ : : : + : : : : + : s2 : : : + : +-----------+-------+ : : +----+----+----+ : + : | ptr | o---+---+-----+-->| C | p | p | : + : | len | 3 | : : +----+----+----+ : + : | capacity | 3 | : : : + : +-----------+-------+ : : : + : : `- - - - - - - - - - - -' + `- - - - - - - - - - - - - -' + +Key points: + +- C++ has made a slightly different choice than Rust. Because :rust:`=` + copies data, the string data has to be cloned. Otherwise we would get + a double-free when either string goes out of scope. + +- C++ also has + `std::move `__, + which is used to indicate when a value may be moved from. If the + example had been :cpp:`s2 = std::move(s1)`, no heap allocation would + take place. After the move, :rust:`s1` would be in a valid but + unspecified state. Unlike Rust, the programmer is allowed to keep + using :rust:`s1`. + +- Unlike Rust, :rust:`=` in C++ can run arbitrary code as determined by the + type which is being copied or moved. diff --git a/courses/comprehensive_rust_training/130_memory_management/05_clone.rst b/courses/comprehensive_rust_training/130_memory_management/05_clone.rst new file mode 100644 index 000000000..ca61caddf --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/05_clone.rst @@ -0,0 +1,40 @@ +======= +Clone +======= + +------- +Clone +------- + +Sometimes you *want* to make a copy of a value. The :rust:`Clone` trait +accomplishes this. + +.. code:: rust + + fn say_hello(name: String) { + println!("Hello {name}") + } + + fn main() { + let name = String::from("Alice"); + say_hello(name.clone()); + say_hello(name); + } + +--------- +Details +--------- + +- The idea of :rust:`Clone` is to make it easy to spot where heap + allocations are occurring. Look for :rust:`.clone()` and a few others + like :rust:`vec!` or :rust:`Box::new`. + +- It's common to "clone your way out" of problems with the borrow + checker, and return later to try to optimize those clones away. + +- :rust:`clone` generally performs a deep copy of the value, meaning that + if you e.g. clone an array, all of the elements of the array are + cloned as well. + +- The behavior for :rust:`clone` is user-defined, so it can perform custom + cloning logic if needed. diff --git a/courses/comprehensive_rust_training/130_memory_management/06_copy_types.rst b/courses/comprehensive_rust_training/130_memory_management/06_copy_types.rst new file mode 100644 index 000000000..18c45f14e --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/06_copy_types.rst @@ -0,0 +1,69 @@ +============ +Copy Types +============ + +------------ +Copy Types +------------ + +While move semantics are the default, certain types are copied by +default: + +.. code:: rust + + fn main() { + let x = 42; + let y = x; + println!("x: {x}"); // would not be accessible if not Copy + println!("y: {y}"); + } + +These types implement the :rust:`Copy` trait. + +You can opt-in your own types to use copy semantics: + +.. code:: rust + + #[derive(Copy, Clone, Debug)] + struct Point(i32, i32); + + fn main() { + let p1 = Point(3, 4); + let p2 = p1; + println!("p1: {p1:?}"); + println!("p2: {p2:?}"); + } + +- After the assignment, both :rust:`p1` and :rust:`p2` own their own data. +- We can also use :rust:`p1.clone()` to explicitly copy the data. + +--------- +Details +--------- + +Copying and cloning are not the same thing: + +- Copying refers to bitwise copies of memory regions and does not work + on arbitrary objects. +- Copying does not allow for custom logic (unlike copy constructors in + C++). +- Cloning is a more general operation and also allows for custom + behavior by implementing the :rust:`Clone` trait. +- Copying does not work on types that implement the :rust:`Drop` trait. + +In the above example, try the following: + +- Add a :rust:`String` field to :rust:`struct Point`. It will not compile + because :rust:`String` is not a :rust:`Copy` type. +- Remove :rust:`Copy` from the :rust:`derive` attribute. The compiler error is + now in the :rust:`println!` for :rust:`p1`. +- Show that it works if you clone :rust:`p1` instead. + +----------------- +More to Explore +----------------- + +- Shared references are :rust:`Copy`/:rust:`Clone`, mutable references are not. + This is because Rust requires that mutable references be exclusive, + so while it's valid to make a copy of a shared reference, creating a + copy of a mutable reference would violate Rust's borrowing rules. diff --git a/courses/comprehensive_rust_training/130_memory_management/07_drop.rst b/courses/comprehensive_rust_training/130_memory_management/07_drop.rst new file mode 100644 index 000000000..40e58fc60 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/07_drop.rst @@ -0,0 +1,68 @@ +==================== +The "Drop" Trait +==================== + +-------------------- +The "Drop" Trait +-------------------- + +Values which implement +`Drop `__ can +specify code to run when they go out of scope: + +.. code:: rust + + struct Droppable { + name: &'static str, + } + + impl Drop for Droppable { + fn drop(&mut self) { + println!("Dropping {}", self.name); + } + } + + fn main() { + let a = Droppable { name: "a" }; + { + let b = Droppable { name: "b" }; + { + let c = Droppable { name: "c" }; + let d = Droppable { name: "d" }; + println!("Exiting block B"); + } + println!("Exiting block A"); + } + drop(a); + println!("Exiting main"); + } + +--------- +Details +--------- + +- Note that :rust:`std::mem::drop` is not the same as + :rust:`std::ops::Drop::drop`. +- Values are automatically dropped when they go out of scope. +- When a value is dropped, if it implements :rust:`std::ops::Drop` then its + :rust:`Drop::drop` implementation will be called. +- All its fields will then be dropped too, whether or not it implements + :rust:`Drop`. +- :rust:`std::mem::drop` is just an empty function that takes any value. + The significance is that it takes ownership of the value, so at the + end of its scope it gets dropped. This makes it a convenient way to + explicitly drop values earlier than they would otherwise go out of + scope. + + - This can be useful for objects that do some work on :rust:`drop`: + releasing locks, closing files, etc. + +Discussion points: + +- Why doesn't :rust:`Drop::drop` take :rust:`self`? + + - Short-answer: If it did, :rust:`std::mem::drop` would be called at the + end of the block, resulting in another call to :rust:`Drop::drop`, and + a stack overflow! + +- Try replacing :rust:`drop(a)` with :rust:`a.drop()`. diff --git a/courses/comprehensive_rust_training/130_memory_management/08_exercise.rst b/courses/comprehensive_rust_training/130_memory_management/08_exercise.rst new file mode 100644 index 000000000..86d9a9ca7 --- /dev/null +++ b/courses/comprehensive_rust_training/130_memory_management/08_exercise.rst @@ -0,0 +1,44 @@ +======================== +Exercise: Builder Type +======================== + +------------------------ +Exercise: Builder Type +------------------------ + +In this example, we will implement a complex data type that owns all of +its data. We will use the "builder pattern" to support building a new +value piece-by-piece, using convenience functions. + +Fill in the missing pieces. + +.. code:: rust + + {{#include exercise.rs:Package}} + {{#include exercise.rs:as_dependency}} + todo!("1") + } + } + + {{#include exercise.rs:PackageBuilder}} + {{#include exercise.rs:new}} + todo!("2") + } + + {{#include exercise.rs:version}} + + {{#include exercise.rs:authors}} + todo!("3") + } + + {{#include exercise.rs:dependency}} + todo!("4") + } + + {{#include exercise.rs:language}} + todo!("5") + } + + {{#include exercise.rs:build}} + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/140_smart_pointers.rst b/courses/comprehensive_rust_training/140_smart_pointers.rst new file mode 100644 index 000000000..9ff353a0b --- /dev/null +++ b/courses/comprehensive_rust_training/140_smart_pointers.rst @@ -0,0 +1,42 @@ +**************** +Smart Pointers +**************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 140_smart_pointers/01_box.rst +.. include:: 140_smart_pointers/02_rc.rst +.. include:: 140_smart_pointers/03_trait_objects.rst +.. include:: 140_smart_pointers/04_exercise.rst diff --git a/courses/comprehensive_rust_training/140_smart_pointers/01_box.rst b/courses/comprehensive_rust_training/140_smart_pointers/01_box.rst new file mode 100644 index 000000000..6fbc7740d --- /dev/null +++ b/courses/comprehensive_rust_training/140_smart_pointers/01_box.rst @@ -0,0 +1,100 @@ +============ +"Box" +============ + +------------ +"Box" +------------ + +`Box `__ is an +owned pointer to data on the heap: + +.. code:: rust + + fn main() { + let five = Box::new(5); + println!("five: {}", *five); + } + +.. code:: bob + + Stack Heap + .- - - - - - -. .- - - - - - -. + : : : : + : five : : : + : +-----+ : : +-----+ : + : | o---|---+-----+-->| 5 | : + : +-----+ : : +-----+ : + : : : : + : : : : + `- - - - - - -' `- - - - - - -' + +:rust:`Box` implements :rust:`Deref`, which means that you can +`call methods from T directly on a Box `__. + +Recursive data types or data types with dynamic sizes cannot be stored +inline without a pointer indirection. :rust:`Box` accomplishes that +indirection: + +.. code:: rust + + #[derive(Debug)] + enum List { + /// A non-empty list: first element and the rest of the list. + Element(T, Box>), + /// An empty list. + Nil, + } + + fn main() { + let list: List = + List::Element(1, Box::new(List::Element(2, Box::new(List::Nil)))); + println!("{list:?}"); + } + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - - . .- - - - - - - - - - - - - - - - - - - - - - - - -. + : : : : + : list : : : + : +---------+----+----+ : : +---------+----+----+ +------+----+----+ : + : | Element | 1 | o--+----+-----+--->| Element | 2 | o--+--->| Nil | // | // | : + : +---------+----+----+ : : +---------+----+----+ +------+----+----+ : + : : : : + : : : : + '- - - - - - - - - - - - - - ' '- - - - - - - - - - - - - - - - - - - - - - - - -' + +--------- +Details +--------- + +- :rust:`Box` is like :rust:`std::unique_ptr` in C++, except that it's + guaranteed to be not null. + +- A :rust:`Box` can be useful when you: + + - have a type whose size can't be known at compile time, but the + Rust compiler wants to know an exact size. + - want to transfer ownership of a large amount of data. To avoid + copying large amounts of data on the stack, instead store the data + on the heap in a :rust:`Box` so only the pointer is moved. + +- If :rust:`Box` was not used and we attempted to embed a :rust:`List` directly + into the :rust:`List`, the compiler would not be able to compute a fixed + size for the struct in memory (the :rust:`List` would be of infinite + size). + +- :rust:`Box` solves this problem as it has the same size as a regular + pointer and just points at the next element of the :rust:`List` in the + heap. + +- Remove the :rust:`Box` in the List definition and show the compiler + error. We get the message "recursive without indirection", because + for data recursion, we have to use indirection, a :rust:`Box` or + reference of some kind, instead of storing the value directly. + +- Though :rust:`Box` looks like :rust:`std::unique_ptr` in C++, it cannot be + empty/null. This makes :rust:`Box` one of the types that allow the + compiler to optimize storage of some enums (the "niche + optimization"). diff --git a/courses/comprehensive_rust_training/140_smart_pointers/02_rc.rst b/courses/comprehensive_rust_training/140_smart_pointers/02_rc.rst new file mode 100644 index 000000000..d8c46b130 --- /dev/null +++ b/courses/comprehensive_rust_training/140_smart_pointers/02_rc.rst @@ -0,0 +1,47 @@ +======== +"Rc" +======== + +-------- +"Rc" +-------- + +`Rc `__ is a +reference-counted shared pointer. Use this when you need to refer to the +same data from multiple places: + +.. code:: rust + + use std::rc::Rc; + + fn main() { + let a = Rc::new(10); + let b = Rc::clone(&a); + + println!("a: {a}"); + println!("b: {b}"); + } + +- See :rust:`Arc`` <../concurrency/shared-state/arc.md>`__ and + `Mutex `__ + if you are in a multi-threaded context. +- You can *downgrade* a shared pointer into a + `Weak `__ + pointer to create cycles that will get dropped. + +--------- +Details +--------- + +- :rust:`Rc` count ensures that its contained value is valid for as + long as there are references. +- :rust:`Rc` in Rust is like :cpp:`std::shared_ptr` in C++. +- :rust:`Rc::clone` is cheap: it creates a pointer to the same allocation + and increases the reference count. Does not make a deep clone and can + generally be ignored when looking for performance issues in code. +- :rust:`make_mut` actually clones the inner value if necessary + ("clone-on-write") and returns a mutable reference. +- Use :rust:`Rc::strong_count` to check the reference count. +- :rust:`Rc::downgrade` gives you a :dfn:`weakly reference-counted` object to + create cycles that will be dropped properly (likely in combination + with :rust:`RefCell`). diff --git a/courses/comprehensive_rust_training/140_smart_pointers/03_trait_objects.rst b/courses/comprehensive_rust_training/140_smart_pointers/03_trait_objects.rst new file mode 100644 index 000000000..a248151ae --- /dev/null +++ b/courses/comprehensive_rust_training/140_smart_pointers/03_trait_objects.rst @@ -0,0 +1,111 @@ +===================== +Owned Trait Objects +===================== + +--------------------- +Owned Trait Objects +--------------------- + +We previously saw how trait objects can be used with references, e.g +:rust:`&dyn Pet`. However, we can also use trait objects with smart pointers +like :rust:`Box` to create an owned trait object: :rust:`Box`. + +.. code:: rust + + struct Dog { + name: String, + age: i8, + } + struct Cat { + lives: i8, + } + + trait Pet { + fn talk(&self) -> String; + } + + impl Pet for Dog { + fn talk(&self) -> String { + format!("Woof, my name is {}!", self.name) + } + } + + impl Pet for Cat { + fn talk(&self) -> String { + String::from("Miau!") + } + } + + fn main() { + let pets: Vec> = vec![ + Box::new(Cat { lives: 9 }), + Box::new(Dog { name: String::from("Fido"), age: 5 }), + ]; + for pet in pets { + println!("Hello, who are you? {}", pet.talk()); + } + } + +Memory layout after allocating :rust:`pets`: + +.. code:: bob + + Stack Heap + .- - - - - - - - - - - - - - - -. .- - - - - - - - - - - - - - - - - - - - - - -. + : : : : + : "pets: Vec>" : : "data: Cat" +----+----+----+----+ : + : +-----------+-------+ : : +-------+-------+ | F | i | d | o | : + : | ptr | o---+-------+--. : | lives | 9 | +----+----+----+----+ : + : | len | 2 | : | : +-------+-------+ ^ : + : | capacity | 2 | : | : ^ | : + : +-----------+-------+ : | : | '-------. : + : : | : | data:"Dog"| : + : : | : | +-------+--|-------+ : + `- - - - - - - - - - - - - - - -' | : +---|-+-----+ | name | o, 4, 4 | : + `--+-->| o o | o o-|----->| age | 5 | : + : +-|---+-|---+ +-------+----------+ : + : | | : + `- - -| - - |- - - - - - - - - - - - - - - - -' + | | + | | "Program text" + .- - -| - - |- - - - - - - - - - - - - - - - -. + : | | vtable : + : | | +----------------------+ : + : | `----->| "::talk" | : + : | +----------------------+ : + : | vtable : + : | +----------------------+ : + : '----------->| "::talk" | : + : +----------------------+ : + : : + '- - - - - - - - - - - - - - - - - - - - - - -' + +--------- +Details +--------- + +- Types that implement a given trait may be of different sizes. This + makes it impossible to have things like :rust:`Vec` in the + example above. + +- :rust:`dyn Pet` is a way to tell the compiler about a dynamically sized + type that implements :rust:`Pet`. + +- In the example, :rust:`pets` is allocated on the stack and the vector + data is on the heap. The two vector elements are *fat pointers*: + + - A fat pointer is a double-width pointer. It has two components: a + pointer to the actual object and a pointer to the + `virtual method table `__ + (vtable) for the :rust:`Pet` implementation of that particular object. + - The data for the :rust:`Dog` named :rust:`"Fido"` is the :rust:`name` and :rust:`age` + fields. The :rust:`Cat` has a :rust:`lives` field. + +- Compare these outputs in the above example: + + .. code:: rust + + println!("{} {}", std::mem::size_of::(), std::mem::size_of::()); + println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>()); + println!("{}", std::mem::size_of::<&dyn Pet>()); + println!("{}", std::mem::size_of::>()); diff --git a/courses/comprehensive_rust_training/140_smart_pointers/04_exercise.rst b/courses/comprehensive_rust_training/140_smart_pointers/04_exercise.rst new file mode 100644 index 000000000..1b3934b7b --- /dev/null +++ b/courses/comprehensive_rust_training/140_smart_pointers/04_exercise.rst @@ -0,0 +1,26 @@ +======================= +Exercise: Binary Tree +======================= + +----------------------- +Exercise: Binary Tree +----------------------- + +A binary tree is a tree-type data structure where every node has two +children (left and right). We will create a tree where each node stores +a value. For a given node N, all nodes in a N's left subtree contain +smaller values, and all nodes in N's right subtree will contain larger +values. + +Implement the following types, so that the given tests pass. + +Extra Credit: implement an iterator over a binary tree that returns the +values in order. + +.. code:: rust + + {{#include exercise.rs:types}} + + // Implement `new`, `insert`, `len`, and `has` for `Subtree`. + + {{#include exercise.rs:tests}} diff --git a/courses/comprehensive_rust_training/150_borrowing.rst b/courses/comprehensive_rust_training/150_borrowing.rst new file mode 100644 index 000000000..17b9f5f84 --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing.rst @@ -0,0 +1,43 @@ +*********** +Borrowing +*********** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 150_borrowing/01_shared.rst +.. include:: 150_borrowing/02_borrowck.rst +.. include:: 150_borrowing/03_examples.rst +.. include:: 150_borrowing/04_interior_mutability.rst +.. include:: 150_borrowing/05_exercise.rst diff --git a/courses/comprehensive_rust_training/150_borrowing/01_shared.rst b/courses/comprehensive_rust_training/150_borrowing/01_shared.rst new file mode 100644 index 000000000..abe9682c1 --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing/01_shared.rst @@ -0,0 +1,83 @@ +=================== +Borrowing a Value +=================== + +------------------- +Borrowing a Value +------------------- + +As we saw before, instead of transferring ownership when calling a +function, you can let a function *borrow* the value: + +.. code:: rust + + #[derive(Debug)] + struct Point(i32, i32); + + fn add(p1: &Point, p2: &Point) -> Point { + Point(p1.0 + p2.0, p1.1 + p2.1) + } + + fn main() { + let p1 = Point(3, 4); + let p2 = Point(10, 20); + let p3 = add(&p1, &p2); + println!("{p1:?} + {p2:?} = {p3:?}"); + } + +- The :rust:`add` function *borrows* two points and returns a new point. +- The caller retains ownership of the inputs. + +--------- +Details +--------- + +This slide is a review of the material on references from day 1, +expanding slightly to include function arguments and return values. + +================= +More to Explore +================= + +----------------- +More to Explore +----------------- + +Notes on stack returns and inlining: + +- Demonstrate that the return from :rust:`add` is cheap because the + compiler can eliminate the copy operation, by inlining the call to + add into main. Change the above code to print stack addresses and run + it on the + `Playground `__ + or look at the assembly in `Godbolt `__. + In the "DEBUG" optimization level, the addresses should change, while + they stay the same when changing to the "RELEASE" setting: + + .. code:: rust + + #[derive(Debug)] + struct Point(i32, i32); + + fn add(p1: &Point, p2: &Point) -> Point { + let p = Point(p1.0 + p2.0, p1.1 + p2.1); + println!("&p.0: {:p}", &p.0); + p + } + + pub fn main() { + let p1 = Point(3, 4); + let p2 = Point(10, 20); + let p3 = add(&p1, &p2); + println!("&p3.0: {:p}", &p3.0); + println!("{p1:?} + {p2:?} = {p3:?}"); + } + +- The Rust compiler can do automatic inlining, that can be disabled on + a function level with :rust:`#[inline(never)]`. + +- Once disabled, the printed address will change on all optimization + levels. Looking at Godbolt or Playground, one can see that in this + case, the return of the value depends on the ABI, e.g. on amd64 the + two i32 that is making up the point will be returned in 2 registers + (eax and edx). diff --git a/courses/comprehensive_rust_training/150_borrowing/02_borrowck.rst b/courses/comprehensive_rust_training/150_borrowing/02_borrowck.rst new file mode 100644 index 000000000..ff1526a51 --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing/02_borrowck.rst @@ -0,0 +1,70 @@ +================= +Borrow Checking +================= + +----------------- +Borrow Checking +----------------- + +Rust's *borrow checker* puts constraints on the ways you can borrow +values. We've already seen that a reference cannot *outlive* the value +it borrows: + +.. code:: rust + + fn main() { + let x_ref = { + let x = 10; + &x + }; + println!("x: {x_ref}"); + } + +There's also a second main rule that the borrow checker enforces: The +*aliasing* rule. For a given value, at any time: + +- You can have one or more shared references to the value, *or* +- You can have exactly one exclusive reference to the value. + +.. code:: rust + + fn main() { + let mut a: i32 = 10; + let b: &i32 = &a; + + { + let c: &mut i32 = &mut a; + *c = 20; + } + + println!("a: {a}"); + println!("b: {b}"); + } + +--------- +Details +--------- + +- The "outlives" rule was demonstrated previously when we first looked + at references. We review it here to show students that the borrow + checking is following a few different rules to validate borrowing. +- Note that the requirement is that conflicting references not *exist* + at the same point. It does not matter where the reference is + dereferenced. +- The above code does not compile because :rust:`a` is borrowed as mutable + (through :rust:`c`) and as immutable (through :rust:`b`) at the same time. +- Move the :rust:`println!` statement for :rust:`b` before the scope that + introduces :rust:`c` to make the code compile. +- After that change, the compiler realizes that :rust:`b` is only ever used + before the new mutable borrow of :rust:`a` through :rust:`c`. This is a + feature of the borrow checker called "non-lexical lifetimes". +- The exclusive reference constraint is quite strong. Rust uses it to + ensure that data races do not occur. Rust also *relies* on this + constraint to optimize code. For example, a value behind a shared + reference can be safely cached in a register for the lifetime of that + reference. +- The borrow checker is designed to accommodate many common patterns, + such as taking exclusive references to different fields in a struct + at the same time. But, there are some situations where it doesn't + quite "get it" and this often results in "fighting with the borrow + checker." diff --git a/courses/comprehensive_rust_training/150_borrowing/03_examples.rst b/courses/comprehensive_rust_training/150_borrowing/03_examples.rst new file mode 100644 index 000000000..a5f0944d3 --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing/03_examples.rst @@ -0,0 +1,39 @@ +=============== +Borrow Errors +=============== + +--------------- +Borrow Errors +--------------- + +As a concrete example of how these borrowing rules prevent memory +errors, consider the case of modifying a collection while there are +references to its elements: + +.. code:: rust + + fn main() { + let mut vec = vec![1, 2, 3, 4, 5]; + let elem = &vec[2]; + vec.push(6); + println!("{elem}"); + } + +Similarly, consider the case of iterator invalidation: + +.. code:: rust + + fn main() { + let mut vec = vec![1, 2, 3, 4, 5]; + for elem in &vec { + vec.push(elem * 2); + } + } + +--------- +Details +--------- + +- In both of these cases, modifying the collection by pushing new + elements into it can potentially invalidate existing references to + the collection's elements if the collection has to reallocate. diff --git a/courses/comprehensive_rust_training/150_borrowing/04_interior_mutability.rst b/courses/comprehensive_rust_training/150_borrowing/04_interior_mutability.rst new file mode 100644 index 000000000..9616cf9ab --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing/04_interior_mutability.rst @@ -0,0 +1,104 @@ +===================== +Interior Mutability +===================== + +--------------------- +Interior Mutability +--------------------- + +In some situations, it's necessary to modify data behind a shared +(read-only) reference. For example, a shared data structure might have +an internal cache, and wish to update that cache from read-only methods. + +The "interior mutability" pattern allows exclusive (mutable) access +behind a shared reference. The standard library provides several ways to +do this, all while still ensuring safety, typically by performing a +runtime check. + +--------- +Details +--------- + +The main thing to take away from this slide is that Rust provides *safe* +ways to modify data behind a shared reference. There are a variety of +ways to ensure that safety, and the next sub-slides present a few of +them. + +-------- +"Cell" +-------- + +:rust:`Cell` wraps a value and allows getting or setting the value using +only a shared reference to the :rust:`Cell`. However, it does not allow +any references to the inner value. Since there are no references, +borrowing rules cannot be broken. + +.. code:: rust + + use std::cell::Cell; + + fn main() { + // Note that "cell" is NOT declared as mutable. + let cell = Cell::new(5); + + cell.set(123); + println!("{}", cell.get()); + } + +---------------- +"Cell" Details +---------------- + +:rust:`Cell` is a simple means to ensure safety: it has a :rust:`set` +method that takes :rust:`&self`. This needs no runtime check, but +requires moving values, which can have its own cost. + +----------- +"RefCell" +----------- + +:rust:`RefCell` allows accessing and mutating a wrapped value by +providing alternative types :rust:`Ref` and :rust:`RefMut` that +emulate :rust:`&T`/:rust:`&mut T` without actually being Rust +references. + +These types perform dynamic checks using a counter in the +:rust:`RefCell` to prevent existence of a :rust:`RefMut` alongside +another :rust:`Ref`/:rust:`RefMut`. + +By implementing :rust:`Deref` (and :rust:`DerefMut` for +:rust:`RefMut`), these types allow calling methods on the inner +value without allowing references to escape. + +.. code:: Rust + + use std::cell::RefCell; + + fn main() { + // Note that "cell" is NOT declared as mutable. + let cell = RefCell::new(5); + + { + let mut cell_ref = cell.borrow_mut(); + *cell_ref = 123; + + // This triggers an error at runtime. + // let other = cell.borrow(); + // println!("{}", *other); + } + + println!("{cell:?}"); + } + +------------------- +"RefCell" Details +------------------- + +- :rust:`RefCell` enforces Rust's usual borrowing rules (either multiple shared + references or a single exclusive reference) with a runtime check. In this + case, all borrows are very short and never overlap, so the checks always + succeed. + +- The extra block in the example is to end the borrow created by the call to + :rust:`borrow_mut` before we print the cell. Trying to print a borrowed + :rust:`RefCell` just shows the message :rust:`"{borrowed}"`. diff --git a/courses/comprehensive_rust_training/150_borrowing/05_exercise.rst b/courses/comprehensive_rust_training/150_borrowing/05_exercise.rst new file mode 100644 index 000000000..bae5268bd --- /dev/null +++ b/courses/comprehensive_rust_training/150_borrowing/05_exercise.rst @@ -0,0 +1,28 @@ +============================= +Exercise: Health Statistics +============================= + +----------------------------- +Exercise: Health Statistics +----------------------------- + +{{#include ../../third_party/rust-on-exercism/health-statistics.md}} + +Copy the code below to https://play.rust-lang.org/ and fill in the +missing method: + +.. code:: rust + + // TODO: remove this when you're done with your implementation. + #![allow(unused_variables, dead_code)] + + {{#include ../../third_party/rust-on-exercism/health-statistics.rs:setup}} + + {{#include ../../third_party/rust-on-exercism/health-statistics.rs:User_visit_doctor}} + todo!("Update a user's statistics based on measurements from a visit to the doctor") + } + } + + {{#include ../../third_party/rust-on-exercism/health-statistics.rs:main}} + + {{#include ../../third_party/rust-on-exercism/health-statistics.rs:tests}} diff --git a/courses/comprehensive_rust_training/160_lifetimes.rst b/courses/comprehensive_rust_training/160_lifetimes.rst new file mode 100644 index 000000000..e2fc4be60 --- /dev/null +++ b/courses/comprehensive_rust_training/160_lifetimes.rst @@ -0,0 +1,42 @@ +*********** +Lifetimes +*********** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 160_lifetimes/01_lifetime_annotations.rst +.. include:: 160_lifetimes/02_lifetime_elision.rst +.. include:: 160_lifetimes/03_struct_lifetimes.rst +.. include:: 160_lifetimes/04_exercise.rst diff --git a/courses/comprehensive_rust_training/160_lifetimes/01_lifetime_annotations.rst b/courses/comprehensive_rust_training/160_lifetimes/01_lifetime_annotations.rst new file mode 100644 index 000000000..585645bc0 --- /dev/null +++ b/courses/comprehensive_rust_training/160_lifetimes/01_lifetime_annotations.rst @@ -0,0 +1,66 @@ +====================== +Lifetime Annotations +====================== + +---------------------- +Lifetime Annotations +---------------------- + +A reference has a *lifetime*, which must not "outlive" the value it +refers to. This is verified by the borrow checker. + +The lifetime can be implicit - this is what we have seen so far. +Lifetimes can also be explicit: :rust:`&'a Point`, :rust:`&'document str`. +Lifetimes start with :rust:`'` and :rust:`'a` is a typical default name. Read +:rust:`&'a Point` as "a borrowed :rust:`Point` which is valid for at least the +lifetime :rust:`a`". + +Lifetimes are always inferred by the compiler: you cannot assign a +lifetime yourself. Explicit lifetime annotations create constraints +where there is ambiguity; the compiler verifies that there is a valid +solution. + +Lifetimes become more complicated when considering passing values to and +returning values from functions. + +.. code:: rust + + #[derive(Debug)] + struct Point(i32, i32); + + fn left_most(p1: &Point, p2: &Point) -> &Point { + if p1.0 < p2.0 { + p1 + } else { + p2 + } + } + + fn main() { + let p1: Point = Point(10, 10); + let p2: Point = Point(20, 20); + let p3 = left_most(&p1, &p2); // What is the lifetime of p3? + println!("p3: {p3:?}"); + } + +--------- +Details +--------- + +In this example, the compiler does not know what lifetime to infer for +:rust:`p3`. Looking inside the function body shows that it can only safely +assume that :rust:`p3`\ 's lifetime is the shorter of :rust:`p1` and :rust:`p2`. But +just like types, Rust requires explicit annotations of lifetimes on +function arguments and return values. + +Add :rust:`'a` appropriately to :rust:`left_most`: + +.. code:: rust + + fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point { + +This says, "given p1 and p2 which both outlive :rust:`'a`, the return value +lives for at least :rust:`'a`. + +In common cases, lifetimes can be elided, as described on the next +slide. diff --git a/courses/comprehensive_rust_training/160_lifetimes/02_lifetime_elision.rst b/courses/comprehensive_rust_training/160_lifetimes/02_lifetime_elision.rst new file mode 100644 index 000000000..9b7807547 --- /dev/null +++ b/courses/comprehensive_rust_training/160_lifetimes/02_lifetime_elision.rst @@ -0,0 +1,73 @@ +============================= +Lifetimes in Function Calls +============================= + +----------------------------- +Lifetimes in Function Calls +----------------------------- + +Lifetimes for function arguments and return values must be fully +specified, but Rust allows lifetimes to be elided in most cases with +`a few simple rules `__. This +is not inference - it is just a syntactic shorthand. + +- Each argument which does not have a lifetime annotation is given one. +- If there is only one argument lifetime, it is given to all + un-annotated return values. +- If there are multiple argument lifetimes, but the first one is for + :rust:`self`, that lifetime is given to all un-annotated return values. + +.. code:: rust + + #[derive(Debug)] + struct Point(i32, i32); + + fn cab_distance(p1: &Point, p2: &Point) -> i32 { + (p1.0 - p2.0).abs() + (p1.1 - p2.1).abs() + } + + fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> { + let mut nearest = None; + for p in points { + if let Some((_, nearest_dist)) = nearest { + let dist = cab_distance(p, query); + if dist < nearest_dist { + nearest = Some((p, dist)); + } + } else { + nearest = Some((p, cab_distance(p, query))); + }; + } + nearest.map(|(p, _)| p) + } + + fn main() { + let points = &[Point(1, 0), Point(1, 0), Point(-1, 0), Point(0, -1)]; + println!("{:?}", nearest(points, &Point(0, 2))); + } + +--------- +Details +--------- + +In this example, :rust:`cab_distance` is trivially elided. + +The :rust:`nearest` function provides another example of a function with +multiple references in its arguments that requires explicit annotation. + +Try adjusting the signature to "lie" about the lifetimes returned: + +.. code:: rust + + fn nearest<'a, 'q>(points: &'a [Point], query: &'q Point) -> Option<&'q Point> { + +This won't compile, demonstrating that the annotations are checked for +validity by the compiler. Note that this is not the case for raw +pointers (unsafe), and this is a common source of errors with unsafe +Rust. + +Students may ask when to use lifetimes. Rust borrows *always* have +lifetimes. Most of the time, elision and type inference mean these don't +need to be written out. In more complicated cases, lifetime annotations +can help resolve ambiguity. Often, especially when prototyping, it's +easier to just work with owned data by cloning values where necessary. diff --git a/courses/comprehensive_rust_training/160_lifetimes/03_struct_lifetimes.rst b/courses/comprehensive_rust_training/160_lifetimes/03_struct_lifetimes.rst new file mode 100644 index 000000000..c0e4b68d6 --- /dev/null +++ b/courses/comprehensive_rust_training/160_lifetimes/03_struct_lifetimes.rst @@ -0,0 +1,47 @@ +============================== +Lifetimes in Data Structures +============================== + +------------------------------ +Lifetimes in Data Structures +------------------------------ + +If a data type stores borrowed data, it must be annotated with a +lifetime: + +.. code:: rust + + #[derive(Debug)] + struct Highlight<'doc>(&'doc str); + + fn erase(text: String) { + println!("Bye {text}!"); + } + + fn main() { + let text = String::from("The quick brown fox jumps over the lazy dog."); + let fox = Highlight(&text[4..19]); + let dog = Highlight(&text[35..43]); + // erase(text); + println!("{fox:?}"); + println!("{dog:?}"); + } + +--------- +Details +--------- + +- In the above example, the annotation on :rust:`Highlight` enforces that + the data underlying the contained :rust:`&str` lives at least as long as + any instance of :rust:`Highlight` that uses that data. +- If :rust:`text` is consumed before the end of the lifetime of :rust:`fox` (or + :rust:`dog`), the borrow checker throws an error. +- Types with borrowed data force users to hold on to the original data. + This can be useful for creating lightweight views, but it generally + makes them somewhat harder to use. +- When possible, make data structures own their data directly. +- Some structs with multiple references inside can have more than one + lifetime annotation. This can be necessary if there is a need to + describe lifetime relationships between the references themselves, in + addition to the lifetime of the struct itself. Those are very + advanced use cases. diff --git a/courses/comprehensive_rust_training/160_lifetimes/04_exercise.rst b/courses/comprehensive_rust_training/160_lifetimes/04_exercise.rst new file mode 100644 index 000000000..47c14c1fe --- /dev/null +++ b/courses/comprehensive_rust_training/160_lifetimes/04_exercise.rst @@ -0,0 +1,111 @@ +============================ +Exercise: Protobuf Parsing +============================ + +---------------------------- +Exercise: Protobuf Parsing +---------------------------- + +In this exercise, you will build a parser for the +`protobuf binary encoding `__. Don't +worry, it's simpler than it seems! This illustrates a common parsing +pattern, passing slices of data. The underlying data itself is never +copied. + +Fully parsing a protobuf message requires knowing the types of the +fields, indexed by their field numbers. That is typically provided in a +:rust:`proto` file. In this exercise, we'll encode that information into +:rust:`match` statements in functions that get called for each field. + +We'll use the following proto: + +.. code:: proto + + message PhoneNumber { + optional string number = 1; + optional string type = 2; + } + + message Person { + optional string name = 1; + optional int32 id = 2; + repeated PhoneNumber phones = 3; + } + +---------- +Messages +---------- + +A proto message is encoded as a series of fields, one after the next. +Each is implemented as a "tag" followed by the value. The tag contains a +field number (e.g., :rust:`2` for the :rust:`id` field of a :rust:`Person` message) +and a wire type defining how the payload should be determined from the +byte stream. These are combined into a single integer, as decoded in +:rust:`unpack_tag` below. + +-------- +Varint +-------- + +Integers, including the tag, are represented with a variable-length +encoding called VARINT. Luckily, :rust:`parse_varint` is defined for you +below. + +------------ +Wire Types +------------ + +Proto defines several wire types, only two of which are used in this +exercise. + +The :rust:`Varint` wire type contains a single varint, and is used to encode +proto values of type :rust:`int32` such as :rust:`Person.id`. + +The :rust:`Len` wire type contains a length expressed as a varint, followed +by a payload of that number of bytes. This is used to encode proto +values of type :rust:`string` such as :rust:`Person.name`. It is also used to +encode proto values containing sub-messages such as :rust:`Person.phones`, +where the payload contains an encoding of the sub-message. + +---------- +Exercise +---------- + +The given code also defines callbacks to handle :rust:`Person` and +:rust:`PhoneNumber` fields, and to parse a message into a series of calls to +those callbacks. + +What remains for you is to implement the :rust:`parse_field` function and +the :rust:`ProtoMessage` trait for :rust:`Person` and :rust:`PhoneNumber`. + +.. code:: rust + + {{#include exercise.rs:preliminaries }} + + + {{#include exercise.rs:parse_field }} + _ => todo!("Based on the wire type, build a Field, consuming as many bytes as necessary.") + }; + todo!("Return the field, and any un-consumed bytes.") + } + + {{#include exercise.rs:parse_message }} + + {{#include exercise.rs:message_phone_number_type}} + + {{#include exercise.rs:message_person_type}} + + // TODO: Implement ProtoMessage for Person and PhoneNumber. + + {{#include exercise.rs:main }} + +--------- +Details +--------- + +- In this exercise there are various cases where protobuf parsing might + fail, e.g. if you try to parse an :rust:`i32` when there are fewer than 4 + bytes left in the data buffer. In normal Rust code we'd handle this + with the :rust:`Result` enum, but for simplicity in this exercise we + panic if any errors are encountered. On day 4 we'll cover error + handling in Rust in more detail. diff --git a/courses/comprehensive_rust_training/170_iterators.rst b/courses/comprehensive_rust_training/170_iterators.rst new file mode 100644 index 000000000..b3c922f17 --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators.rst @@ -0,0 +1,44 @@ +*********** +Iterators +*********** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 170_iterators/01_motivation.rst +.. include:: 170_iterators/02_iterator.rst +.. include:: 170_iterators/03_helpers.rst +.. include:: 170_iterators/04_collect.rst +.. include:: 170_iterators/05_intoiterator.rst +.. include:: 170_iterators/06_exercise.rst diff --git a/courses/comprehensive_rust_training/170_iterators/01_motivation.rst b/courses/comprehensive_rust_training/170_iterators/01_motivation.rst new file mode 100644 index 000000000..85aa0b03d --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/01_motivation.rst @@ -0,0 +1,68 @@ +====================== +Motivating Iterators +====================== + +---------------------- +Motivating Iterators +---------------------- + +If you want to iterate over the contents of an array, you'll need to +define: + +- Some state to keep track of where you are in the iteration process, + e.g. an index. +- A condition to determine when iteration is done. +- Logic for updating the state of iteration each loop. +- Logic for fetching each element using that iteration state. + +In a C-style for loop you declare these things directly: + +.. code:: c + + for (int i = 0; i < array_len; i += 1) { + int elem = array[i]; + } + +In Rust we bundle this state and logic together into an object known as +an "iterator". + +--------- +Details +--------- + +- This slide provides context for what Rust iterators do under the + hood. We use the (hopefully) familiar construct of a C-style :cpp:`for` + loop to show how iteration requires some state and some logic, that + way on the next slide we can show how an iterator bundles these + together. + +- Rust doesn't have a C-style :rust:`for` loop, but we can express the same + thing with :rust:`while`: + + .. code:: rust + + let array = [2, 4, 6, 8]; + let mut i = 0; + while i < array.len() { + let elem = array[i]; + i += 1; + } + +----------------- +More to Explore +----------------- + +There's another way to express array iteration using :cpp:`for` in C and +C++: You can use a pointer to the front and a pointer to the end of the +array and then compare those pointers to determine when the loop should +end. + +.. code:: c + + for (int *ptr = array; ptr < array + len; ptr += 1) { + int elem = *ptr; + } + +If students ask, you can point out that this is how Rust's slice and +array iterators work under the hood (though implemented as a Rust +iterator). diff --git a/courses/comprehensive_rust_training/170_iterators/02_iterator.rst b/courses/comprehensive_rust_training/170_iterators/02_iterator.rst new file mode 100644 index 000000000..d2020da18 --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/02_iterator.rst @@ -0,0 +1,73 @@ +==================== +"Iterator" Trait +==================== + +-------------------- +"Iterator" Trait +-------------------- + +The +`Iterator `__ +trait defines how an object can be used to produce a sequence of values. +For example, if we wanted to create an iterator that can produce the +elements of a slice it might look something like this: + +.. code:: rust + + struct SliceIter<'s> { + slice: &'s [i32], + i: usize, + } + + impl<'s> Iterator for SliceIter<'s> { + type Item = &'s i32; + + fn next(&mut self) -> Option { + if self.i == self.slice.len() { + None + } else { + let next = &self.slice[self.i]; + self.i += 1; + Some(next) + } + } + } + + fn main() { + let slice = &[2, 4, 6, 8]; + let iter = SliceIter { slice, i: 0 }; + for elem in iter { + println!("elem: {elem}"); + } + } + +--------- +Details +--------- + +- The :rust:`SliceIter` example implements the same logic as the C-style + :rust:`for` loop demonstrated on the last slide. + +- Point out to the students that iterators are lazy: Creating the + iterator just initializes the struct but does not otherwise do any + work. No work happens until the :rust:`next` method is called. + +- Iterators don't need to be finite! It's entirely valid to have an + iterator that will produce values forever. For example, a half open + range like :rust:`0..` will keep going until integer overflow occurs. + +----------------- +More to Explore +----------------- + +- The "real" version of :rust:`SliceIter` is the + `slice::Iter `__ + type in the standard library, however the real version uses pointers + under the hood instead of an index in order to eliminate bounds + checks. + +- The :rust:`SliceIter` example is a good example of a struct that contains + a reference and therefore uses lifetime annotations. + +- You can also demonstrate adding a generic parameter to :rust:`SliceIter` + to allow it to work with any kind of slice (not just :rust:`&[i32]`). diff --git a/courses/comprehensive_rust_training/170_iterators/03_helpers.rst b/courses/comprehensive_rust_training/170_iterators/03_helpers.rst new file mode 100644 index 000000000..b57fe1c1b --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/03_helpers.rst @@ -0,0 +1,47 @@ +============================= +"Iterator" Helper Methods +============================= + +----------------------------- +"Iterator" Helper Methods +----------------------------- + +In addition to the :rust:`next` method that defines how an iterator behaves, +the :rust:`Iterator` trait provides 70+ helper methods that can be used to +build customized iterators. + +.. code:: rust + + let result: i32 = (1..=10) // Create a range from 1 to 10 + .filter(|&x| x % 2 == 0) // Keep only even numbers + .map(|x| x * x) // Square each number + .sum(); // Sum up all the squared numbers + + println!("The sum of squares of even numbers from 1 to 10 is: {}", result); + +--------- +Details +--------- + +- The :rust:`Iterator` trait implements many common functional programming + operations over collections (e.g. :rust:`map`, :rust:`filter`, :rust:`reduce`, + etc). This is the trait where you can find all the documentation + about them. + +- Many of these helper methods take the original iterator and produce a + new iterator with different behavior. These are know as "iterator + adapter methods". + +- Some methods, like :rust:`sum` and :rust:`count`, consume the iterator and + pull all of the elements out of it. + +- These methods are designed to be chained together so that it's easy + to build a custom iterator that does exactly what you need. + +----------------- +More to Explore +----------------- + +- Rust's iterators are extremely efficient and highly optimizable. Even + complex iterators made by combining many adapter methods will still + result in code as efficient as equivalent imperative implementations. diff --git a/courses/comprehensive_rust_training/170_iterators/04_collect.rst b/courses/comprehensive_rust_training/170_iterators/04_collect.rst new file mode 100644 index 000000000..2c7f26747 --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/04_collect.rst @@ -0,0 +1,56 @@ +============= +"collect" +============= + +------------- +"collect" +------------- + +The +`collect `__ +method lets you build a collection from an +`Iterator `__. + +.. code:: rust + + fn main() { + let primes = vec![2, 3, 5, 7]; + let prime_squares = primes.into_iter().map(|p| p * p).collect::>(); + println!("prime_squares: {prime_squares:?}"); + } + +--------- +Details +--------- + +- Any iterator can be collected in to a :rust:`Vec`, :rust:`VecDeque`, or + :rust:`HashSet`. Iterators that produce key-value pairs (i.e. a + two-element tuple) can also be collected into :rust:`HashMap` and + :rust:`BTreeMap`. + +Show the students the definition for :rust:`collect` in the standard library +docs. There are two ways to specify the generic type :rust:`B` for this +method: + +- With the "turbofish": :rust:`some_iterator.collect::()`, + as shown. The :rust:`_` shorthand used here lets Rust infer the type of + the :rust:`Vec` elements. +- With type inference: + :rust:`let prime_squares: Vec<_> = some_iterator.collect()`. Rewrite the + example to use this form. + +----------------- +More to Explore +----------------- + +- If students are curious about how this works, you can bring up the + `FromIterator `__ + trait, which defines how each type of collection gets built from an + iterator. +- In addition to the basic implementations of :rust:`FromIterator` for + :rust:`Vec`, :rust:`HashMap`, etc., there are also more specialized + implementations which let you do cool things like convert an + :rust:`Iterator>` into a :rust:`Result, E>`. +- The reason type annotations are often needed with :rust:`collect` is + because it's generic over its return type. This makes it harder for + the compiler to infer the correct type in a lot of cases. diff --git a/courses/comprehensive_rust_training/170_iterators/05_intoiterator.rst b/courses/comprehensive_rust_training/170_iterators/05_intoiterator.rst new file mode 100644 index 000000000..15838928f --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/05_intoiterator.rst @@ -0,0 +1,92 @@ +================== +"IntoIterator" +================== + +------------------ +"IntoIterator" +------------------ + +The :rust:`Iterator` trait tells you how to *iterate* once you have created +an iterator. The related trait +`IntoIterator `__ +defines how to create an iterator for a type. It is used automatically +by the :rust:`for` loop. + +.. code:: rust + + struct Grid { + x_coords: Vec, + y_coords: Vec, + } + + impl IntoIterator for Grid { + type Item = (u32, u32); + type IntoIter = GridIter; + fn into_iter(self) -> GridIter { + GridIter { grid: self, i: 0, j: 0 } + } + } + + struct GridIter { + grid: Grid, + i: usize, + j: usize, + } + + impl Iterator for GridIter { + type Item = (u32, u32); + + fn next(&mut self) -> Option<(u32, u32)> { + if self.i >= self.grid.x_coords.len() { + self.i = 0; + self.j += 1; + if self.j >= self.grid.y_coords.len() { + return None; + } + } + let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j])); + self.i += 1; + res + } + } + + fn main() { + let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] }; + for (x, y) in grid { + println!("point = {x}, {y}"); + } + } + +--------- +Details +--------- + +- :rust:`IntoIterator` is the trait that makes for loops work. It is + implemented by collection types such as :rust:`Vec` and references to + them such as :rust:`&Vec` and :rust:`&[T]`. Ranges also implement it. This + is why you can iterate over a vector with + :rust:`for i in some_vec { .. }` but :rust:`some_vec.next()` doesn't exist. + +Click through to the docs for :rust:`IntoIterator`. Every implementation of +:rust:`IntoIterator` must declare two types: + +- :rust:`Item`: the type to iterate over, such as :rust:`i8`, +- :rust:`IntoIter`: the :rust:`Iterator` type returned by the :rust:`into_iter` + method. + +Note that :rust:`IntoIter` and :rust:`Item` are linked: the iterator must have +the same :rust:`Item` type, which means that it returns :rust:`Option` + +The example iterates over all combinations of x and y coordinates. + +Try iterating over the grid twice in :rust:`main`. Why does this fail? Note +that :rust:`IntoIterator::into_iter` takes ownership of :rust:`self`. + +Fix this issue by implementing :rust:`IntoIterator` for :rust:`&Grid` and +storing a reference to the :rust:`Grid` in :rust:`GridIter`. + +The same problem can occur for standard library types: +:rust:`for e in some_vector` will take ownership of :rust:`some_vector` and +iterate over owned elements from that vector. Use +:rust:`for e in &some_vector` instead, to iterate over references to +elements of :rust:`some_vector`. diff --git a/courses/comprehensive_rust_training/170_iterators/06_exercise.rst b/courses/comprehensive_rust_training/170_iterators/06_exercise.rst new file mode 100644 index 000000000..fc7c432bc --- /dev/null +++ b/courses/comprehensive_rust_training/170_iterators/06_exercise.rst @@ -0,0 +1,24 @@ +==================================== +Exercise: Iterator Method Chaining +==================================== + +------------------------------------ +Exercise: Iterator Method Chaining +------------------------------------ + +In this exercise, you will need to find and use some of the provided +methods in the +`Iterator `__ +trait to implement a complex calculation. + +Copy the following code to https://play.rust-lang.org/ and make the +tests pass. Use an iterator expression and :rust:`collect` the result to +construct the return value. + +.. code:: rust + + {{#include exercise.rs:offset_differences}} + unimplemented!() + } + + {{#include exercise.rs:unit-tests}} diff --git a/courses/comprehensive_rust_training/180_modules.rst b/courses/comprehensive_rust_training/180_modules.rst new file mode 100644 index 000000000..beda480e2 --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules.rst @@ -0,0 +1,44 @@ +********* +Modules +********* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 180_modules/01_modules.rst +.. include:: 180_modules/02_filesystem.rst +.. include:: 180_modules/03_visibility.rst +.. include:: 180_modules/04_encapsulation.rst +.. include:: 180_modules/05_paths.rst +.. include:: 180_modules/06_exercise.rst diff --git a/courses/comprehensive_rust_training/180_modules/01_modules.rst b/courses/comprehensive_rust_training/180_modules/01_modules.rst new file mode 100644 index 000000000..6cef97d19 --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/01_modules.rst @@ -0,0 +1,41 @@ +========= +Modules +========= + +--------- +Modules +--------- + +We have seen how :rust:`impl` blocks let us namespace functions to a type. + +Similarly, :rust:`mod` lets us namespace types and functions: + +.. code:: rust + + mod foo { + pub fn do_something() { + println!("In the foo module"); + } + } + + mod bar { + pub fn do_something() { + println!("In the bar module"); + } + } + + fn main() { + foo::do_something(); + bar::do_something(); + } + +--------- +Details +--------- + +- Packages provide functionality and include a :rust:`Cargo.toml` file that + describes how to build a bundle of 1+ crates. +- Crates are a tree of modules, where a binary crate creates an + executable and a library crate compiles to a library. +- Modules define organization, scope, and are the focus of this + section. diff --git a/courses/comprehensive_rust_training/180_modules/02_filesystem.rst b/courses/comprehensive_rust_training/180_modules/02_filesystem.rst new file mode 100644 index 000000000..f0627aabf --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/02_filesystem.rst @@ -0,0 +1,80 @@ +====================== +Filesystem Hierarchy +====================== + +---------------------- +Filesystem Hierarchy +---------------------- + +Omitting the module content will tell Rust to look for it in another +file: + +.. code:: rust + + mod garden; + +This tells Rust that the :rust:`garden` module content is found at +:rust:`src/garden.rs`. Similarly, a :rust:`garden::vegetables` module can be +found at :rust:`src/garden/vegetables.rs`. + +The :rust:`crate` root is in: + +- :rust:`src/lib.rs` (for a library crate) +- :rust:`src/main.rs` (for a binary crate) + +Modules defined in files can be documented, too, using "inner doc +comments". These document the item that contains them - in this case, a +module. + +.. code:: rust + + //! This module implements the garden, including a highly performant germination + //! implementation. + + // Re-export types from this module. + pub use garden::Garden; + pub use seeds::SeedPacket; + + /// Sow the given seed packets. + pub fn sow(seeds: Vec) { + todo!() + } + + /// Harvest the produce in the garden that is ready. + pub fn harvest(garden: &mut Garden) { + todo!() + } + +--------- +Details +--------- + +- Before Rust 2018, modules needed to be located at :rust:`module/mod.rs` + instead of :rust:`module.rs`, and this is still a working alternative for + editions after 2018. + +- The main reason to introduce :rust:`filename.rs` as alternative to + :rust:`filename/mod.rs` was because many files named :rust:`mod.rs` can be + hard to distinguish in IDEs. + +- Deeper nesting can use folders, even if the main module is a file: + + .. code:: ignore + + src/ + |-- main.rs + |-- top_module.rs + |-- top_module/ + |-- sub_module.rs + +- The place rust will look for modules can be changed with a compiler + directive: + + .. code:: rust + + #[path = "some/path.rs"] + mod some_module; + + This is useful, for example, if you would like to place tests for a + module in a file named :rust:`some_module_test.rs`, similar to the + convention in Go. diff --git a/courses/comprehensive_rust_training/180_modules/03_visibility.rst b/courses/comprehensive_rust_training/180_modules/03_visibility.rst new file mode 100644 index 000000000..d6b26ba86 --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/03_visibility.rst @@ -0,0 +1,56 @@ +============ +Visibility +============ + +------------ +Visibility +------------ + +Modules are a privacy boundary: + +- Module items are private by default (hides implementation details). +- Parent and sibling items are always visible. +- In other words, if an item is visible in module :rust:`foo`, it's visible + in all the descendants of :rust:`foo`. + +.. code:: rust + + mod outer { + fn private() { + println!("outer::private"); + } + + pub fn public() { + println!("outer::public"); + } + + mod inner { + fn private() { + println!("outer::inner::private"); + } + + pub fn public() { + println!("outer::inner::public"); + super::private(); + } + } + } + + fn main() { + outer::public(); + } + +--------- +Details +--------- + +- Use the :rust:`pub` keyword to make modules public. + +Additionally, there are advanced :rust:`pub(...)` specifiers to restrict the +scope of public visibility. + +- See the `Rust Reference `__. +- Configuring :rust:`pub(crate)` visibility is a common pattern. +- Less commonly, you can give visibility to a specific path. +- In any case, visibility must be granted to an ancestor module (and + all of its descendants). diff --git a/courses/comprehensive_rust_training/180_modules/04_encapsulation.rst b/courses/comprehensive_rust_training/180_modules/04_encapsulation.rst new file mode 100644 index 000000000..6fbfd3821 --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/04_encapsulation.rst @@ -0,0 +1,85 @@ +============================== +Visibility and Encapsulation +============================== + +------------------------------ +Visibility and Encapsulation +------------------------------ + +Like with items in a module, struct fields are also private by default. +Private fields are likewise visible within the rest of the module +(including child modules). This allows us to encapsulate implementation +details of struct, controlling what data and functionality is visible +externally. + +.. code:: rust + + use outer::Foo; + + mod outer { + pub struct Foo { + pub val: i32, + is_big: bool, + } + + impl Foo { + pub fn new(val: i32) -> Self { + Self { val, is_big: val > 100 } + } + } + + pub mod inner { + use super::Foo; + + pub fn print_foo(foo: &Foo) { + println!("Is {} big? {}", foo.val, foo.is_big); + } + } + } + + fn main() { + let foo = Foo::new(42); + println!("foo.val = {}", foo.val); + // let foo = Foo { val: 42, is_big: true }; + + outer::inner::print_foo(&foo); + // println!("Is {} big? {}", foo.val, foo.is_big); + } + +--------- +Details +--------- + +- This slide demonstrates how privacy in structs is module-based. + Students coming from object oriented languages may be used to types + being the encapsulation boundary, so this demonstrates how Rust + behaves differently while showing how we can still achieve + encapsulation. + +- Note how the :rust:`is_big` field is fully controlled by :rust:`Foo`, + allowing :rust:`Foo` to control how it's initialized and enforce any + invariants it needs to (e.g. that :rust:`is_big` is only :rust:`true` if + :rust:`val > 100`). + +- Point out how helper functions can be defined in the same module + (including child modules) in order to get access to the type's + private fields/methods. + +- The first commented out line demonstrates that you cannot initialize + a struct with private fields. The second one demonstrates that you + also can't directly access private fields. + +- Enums do not support privacy: Variants and data within those variants + is always public. + +----------------- +More to Explore +----------------- + +- If students want more information about privacy (or lack thereof) in + enums, you can bring up :rust:`#[doc_hidden]` and :rust:`#[non_exhaustive]` + and show how they're used to limit what can be done with an enum. + +- Module privacy still applies when there are :rust:`impl` blocks in other + modules + `(example in the playground) `__. diff --git a/courses/comprehensive_rust_training/180_modules/05_paths.rst b/courses/comprehensive_rust_training/180_modules/05_paths.rst new file mode 100644 index 000000000..766be8c14 --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/05_paths.rst @@ -0,0 +1,58 @@ +================== +use, super, self +================== + +------------------ +use, super, self +------------------ + +A module can bring symbols from another module into scope with :rust:`use`. +You will typically see something like this at the top of each module: + +.. code:: rust + + use std::collections::HashSet; + use std::process::abort; + +------- +Paths +------- + +Paths are resolved as follows: + +1. As a relative path: + + - :rust:`foo` or :rust:`self::foo` refers to :rust:`foo` in the current module, + - :rust:`super::foo` refers to :rust:`foo` in the parent module. + +2. As an absolute path: + + - :rust:`crate::foo` refers to :rust:`foo` in the root of the current crate, + - :rust:`bar::foo` refers to :rust:`foo` in the :rust:`bar` crate. + +--------- +Details +--------- + +- It is common to "re-export" symbols at a shorter path. For example, + the top-level :rust:`lib.rs` in a crate might have + + .. code:: rust + + mod storage; + + pub use storage::disk::DiskStorage; + pub use storage::network::NetworkStorage; + + making :rust:`DiskStorage` and :rust:`NetworkStorage` available to other + crates with a convenient, short path. + +- For the most part, only items that appear in a module need to be + :rust:`use`. However, a trait must be in scope to call any methods on + that trait, even if a type implementing that trait is already in + scope. For example, to use the :rust:`read_to_string` method on a type + implementing the :rust:`Read` trait, you need to :rust:`use std::io::Read`. + +- The :rust:`use` statement can have a wildcard: :rust:`use std::io::*`. This + is discouraged because it is not clear which items are imported, and + those might change over time. diff --git a/courses/comprehensive_rust_training/180_modules/06_exercise.rst b/courses/comprehensive_rust_training/180_modules/06_exercise.rst new file mode 100644 index 000000000..b7727aa8a --- /dev/null +++ b/courses/comprehensive_rust_training/180_modules/06_exercise.rst @@ -0,0 +1,48 @@ +===================================== +Exercise: Modules for a GUI Library +===================================== + +------------------------------------- +Exercise: Modules for a GUI Library +------------------------------------- + +In this exercise, you will reorganize a small GUI Library +implementation. This library defines a :rust:`Widget` trait and a few +implementations of that trait, as well as a :rust:`main` function. + +It is typical to put each type or set of closely-related types into its +own module, so each widget type should get its own module. + +------------- +Cargo Setup +------------- + +The Rust playground only supports one file, so you will need to make a +Cargo project on your local filesystem: + +.. code:: shell + + cargo init gui-modules + cd gui-modules + cargo run + +Edit the resulting :rust:`src/main.rs` to add :rust:`mod` statements, and add +additional files in the :rust:`src` directory. + +-------- +Source +-------- + +Here's the single-module implementation of the GUI library: + +.. code:: rust + + {{#include exercise.rs:single-module}} + +--------- +Details +--------- + +Encourage students to divide the code in a way that feels natural for +them, and get accustomed to the required :rust:`mod`, :rust:`use`, and :rust:`pub` +declarations. Afterward, discuss what organizations are most idiomatic. diff --git a/courses/comprehensive_rust_training/190_testing.rst b/courses/comprehensive_rust_training/190_testing.rst new file mode 100644 index 000000000..68b64b594 --- /dev/null +++ b/courses/comprehensive_rust_training/190_testing.rst @@ -0,0 +1,42 @@ +********* +Testing +********* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 190_testing/01_unit_tests.rst +.. include:: 190_testing/02_other.rst +.. include:: 190_testing/03_lints.rst +.. include:: 190_testing/04_exercise.rst diff --git a/courses/comprehensive_rust_training/190_testing/01_unit_tests.rst b/courses/comprehensive_rust_training/190_testing/01_unit_tests.rst new file mode 100644 index 000000000..742279e56 --- /dev/null +++ b/courses/comprehensive_rust_training/190_testing/01_unit_tests.rst @@ -0,0 +1,51 @@ +============ +Unit Tests +============ + +------------ +Unit Tests +------------ + +Rust and Cargo come with a simple unit test framework. Tests are marked +with :rust:`#[test]`. Unit tests are often put in a nested :rust:`tests` module, +using :rust:`#[cfg(test)]` to conditionally compile them only when building +tests. + +.. code:: rust + + fn first_word(text: &str) -> &str { + match text.find(' ') { + Some(idx) => &text[..idx], + None => &text, + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_empty() { + assert_eq!(first_word(""), ""); + } + + #[test] + fn test_single_word() { + assert_eq!(first_word("Hello"), "Hello"); + } + + #[test] + fn test_multiple_words() { + assert_eq!(first_word("Hello World"), "Hello"); + } + } + +- This lets you unit test private helpers. +- The :rust:`#[cfg(test)]` attribute is only active when you run + :rust:`cargo test`. + +--------- +Details +--------- + +Run the tests in the playground in order to show their results. diff --git a/courses/comprehensive_rust_training/190_testing/02_other.rst b/courses/comprehensive_rust_training/190_testing/02_other.rst new file mode 100644 index 000000000..8dcebceb8 --- /dev/null +++ b/courses/comprehensive_rust_training/190_testing/02_other.rst @@ -0,0 +1,49 @@ +====================== +Other Types of Tests +====================== + +------------------- +Integration Tests +------------------- + +If you want to test your library as a client, use an integration test. + +Create a :rust:`.rs` file under :rust:`tests/`: + +.. code:: rust + + // tests/my_library.rs + use my_library::init; + + #[test] + fn test_init() { + assert!(init().is_ok()); + } + +These tests only have access to the public API of your crate. + +--------------------- +Documentation Tests +--------------------- + +Rust has built-in support for documentation tests: + +.. code:: rust + + /// Shortens a string to the given length. + /// + /// :rust:` + /// # use playground::shorten_string; + /// assert_eq!(shorten_string("Hello World", 5), "Hello"); + /// assert_eq!(shorten_string("Hello World", 20), "Hello World"); + /// :rust:` + pub fn shorten_string(s: &str, length: usize) -> &str { + &s[..std::cmp::min(length, s.len())] + } + +- Code blocks in :rust:`///` comments are automatically seen as Rust code. +- The code will be compiled and executed as part of :rust:`cargo test`. +- Adding :rust:`#` in the code will hide it from the docs, but will still + compile/run it. +- Test the above code on the + `Rust Playground `__. diff --git a/courses/comprehensive_rust_training/190_testing/03_lints.rst b/courses/comprehensive_rust_training/190_testing/03_lints.rst new file mode 100644 index 000000000..6955b590d --- /dev/null +++ b/courses/comprehensive_rust_training/190_testing/03_lints.rst @@ -0,0 +1,34 @@ +=========================== +Compiler Lints and Clippy +=========================== + +--------------------------- +Compiler Lints and Clippy +--------------------------- + +The Rust compiler produces fantastic error messages, as well as helpful +built-in lints. `Clippy `__ provides +even more lints, organized into groups that can be enabled per-project. + +.. code:: rust + + #[deny(clippy::cast_possible_truncation)] + fn main() { + let mut x = 3; + while (x < 70000) { + x *= 2; + } + println!("X probably fits in a u16, right? {}", x as u16); + } + +--------- +Details +--------- + +There are compiler lints visible here, but not clippy lints. Run +:rust:`clippy` on the playground site to show clippy warnings. Clippy has +extensive documentation of its lints, and adds new lints (including +default-deny lints) all the time. + +Note that errors or warnings with :rust:`help: ...` can be fixed with +:rust:`cargo fix` or via your editor. diff --git a/courses/comprehensive_rust_training/190_testing/04_exercise.rst b/courses/comprehensive_rust_training/190_testing/04_exercise.rst new file mode 100644 index 000000000..c8243b27f --- /dev/null +++ b/courses/comprehensive_rust_training/190_testing/04_exercise.rst @@ -0,0 +1,39 @@ +========================== +Exercise: Luhn Algorithm +========================== + +-------------------------- +Exercise: Luhn Algorithm +-------------------------- + +The `Luhn algorithm `__ is +used to validate credit card numbers. The algorithm takes a string as +input and does the following to validate the credit card number: + +- Ignore all spaces. Reject numbers with fewer than two digits. + +- Moving from **right to left**, double every second digit: for the + number :rust:`1234`, we double :rust:`3` and :rust:`1`. For the number :rust:`98765`, + we double :rust:`6` and :rust:`8`. + +- After doubling a digit, sum the digits if the result is greater than + 9. So doubling :rust:`7` becomes :rust:`14` which becomes :rust:`1 + 4 = 5`. + +- Sum all the undoubled and doubled digits. + +- The credit card number is valid if the sum ends with :rust:`0`. + +The provided code provides a buggy implementation of the luhn algorithm, +along with two basic unit tests that confirm that most of the algorithm +is implemented correctly. + +Copy the code below to https://play.rust-lang.org/ and write additional +tests to uncover bugs in the provided implementation, fixing any bugs +you find. + +.. code:: rust + + {{#include exercise.rs:luhn}} + + {{#include exercise.rs:unit-tests}} + } diff --git a/courses/comprehensive_rust_training/200_error_handling.rst b/courses/comprehensive_rust_training/200_error_handling.rst new file mode 100644 index 000000000..e8a88c20b --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling.rst @@ -0,0 +1,46 @@ +**************** +Error Handling +**************** + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 200_error_handling/01_panics.rst +.. include:: 200_error_handling/02_result.rst +.. include:: 200_error_handling/03_try.rst +.. include:: 200_error_handling/04_try_conversions.rst +.. include:: 200_error_handling/05_error.rst +.. include:: 200_error_handling/06_thiserror.rst +.. include:: 200_error_handling/07_anyhow.rst +.. include:: 200_error_handling/08_exercise.rst diff --git a/courses/comprehensive_rust_training/200_error_handling/01_panics.rst b/courses/comprehensive_rust_training/200_error_handling/01_panics.rst new file mode 100644 index 000000000..a1d0dc7d1 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/01_panics.rst @@ -0,0 +1,58 @@ +======== +Panics +======== + +-------- +Panics +-------- + +Rust handles fatal errors with a "panic". + +Rust will trigger a panic if a fatal error happens at runtime: + +.. code:: rust + + fn main() { + let v = vec![10, 20, 30]; + println!("v[100]: {}", v[100]); + } + +- Panics are for unrecoverable and unexpected errors. + + - Panics are symptoms of bugs in the program. + - Runtime failures like failed bounds checks can panic + - Assertions (such as :rust:`assert!`) panic on failure + - Purpose-specific panics can use the :rust:`panic!` macro. + +- A panic will "unwind" the stack, dropping values just as if the + functions had returned. +- Use non-panicking APIs (such as :rust:`Vec::get`) if crashing is not + acceptable. + +--------- +Details +--------- + +By default, a panic will cause the stack to unwind. The unwinding can be +caught: + +.. code:: rust + + use std::panic; + + fn main() { + let result = panic::catch_unwind(|| "No problem here!"); + println!("{result:?}"); + + let result = panic::catch_unwind(|| { + panic!("oh no!"); + }); + println!("{result:?}"); + } + +- Catching is unusual; do not attempt to implement exceptions with + :rust:`catch_unwind`! +- This can be useful in servers which should keep running even if a + single request crashes. +- This does not work if :rust:`panic = 'abort'` is set in your + :rust:`Cargo.toml`. diff --git a/courses/comprehensive_rust_training/200_error_handling/02_result.rst b/courses/comprehensive_rust_training/200_error_handling/02_result.rst new file mode 100644 index 000000000..b7ee3428c --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/02_result.rst @@ -0,0 +1,91 @@ +============ +"Result" +============ + +------------ +"Result" +------------ + +Our primary mechanism for error handling in Rust is the +`Result `__ +enum, which we briefly saw when discussing standard library types. + +.. code:: rust + + use std::fs::File; + use std::io::Read; + + fn main() { + let file: Result = File::open("diary.txt"); + match file { + Ok(mut file) => { + let mut contents = String::new(); + if let Ok(bytes) = file.read_to_string(&mut contents) { + println!("Dear diary: {contents} ({bytes} bytes)"); + } else { + println!("Could not read file content"); + } + } + Err(err) => { + println!("The diary could not be opened: {err}"); + } + } + } + +--------- +Details +--------- + +- :rust:`Result` has two variants: :rust:`Ok` which contains the success value, + and :rust:`Err` which contains an error value of some kind. + +- Whether or not a function can produce an error is encoded in the + function's type signature by having the function return a :rust:`Result` + value. + +- Like with :rust:`Option`, there is no way to forget to handle an error: + You cannot access either the success value or the error value without + first pattern matching on the :rust:`Result` to check which variant you + have. Methods like :rust:`unwrap` make it easier to write quick-and-dirty + code that doesn't do robust error handling, but means that you can + always see in your source code where proper error handling is being + skipped. + +================= +More to Explore +================= + +----------------- +More to Explore +----------------- + +It may be helpful to compare error handling in Rust to error handling +conventions that students may be familiar with from other programming +languages. + +------------ +Exceptions +------------ + +- Many languages use exceptions, e.g. C++, Java, Python. + +- In most languages with exceptions, whether or not a function can + throw an exception is not visible as part of its type signature. This + generally means that you can't tell when calling a function if it may + throw an exception or not. + +- Exceptions generally unwind the call stack, propagating upward until + a :rust:`try` block is reached. An error originating deep in the call + stack may impact an unrelated function further up. + +--------------- +Error Numbers +--------------- + +- Some languages have functions return an error number (or some other + error value) separately from the successful return value of the + function. Examples include C and Go. + +- Depending on the language it may be possible to forget to check the + error value, in which case you may be accessing an uninitialized or + otherwise invalid success value. diff --git a/courses/comprehensive_rust_training/200_error_handling/03_try.rst b/courses/comprehensive_rust_training/200_error_handling/03_try.rst new file mode 100644 index 000000000..615c73710 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/03_try.rst @@ -0,0 +1,69 @@ +============== +Try Operator +============== + +-------------- +Try Operator +-------------- + +Runtime errors like connection-refused or file-not-found are handled +with the :rust:`Result` type, but matching this type on every call can be +cumbersome. The try-operator :rust:`?` is used to return errors to the +caller. It lets you turn the common + +.. code:: rust + + match some_expression { + Ok(value) => value, + Err(err) => return Err(err), + } + +into the much simpler + +.. code:: rust + + some_expression? + +We can use this to simplify our error handling code: + +.. code:: rust + + use std::io::Read; + use std::{fs, io}; + + fn read_username(path: &str) -> Result { + let username_file_result = fs::File::open(path); + let mut username_file = match username_file_result { + Ok(file) => file, + Err(err) => return Err(err), + }; + + let mut username = String::new(); + match username_file.read_to_string(&mut username) { + Ok(_) => Ok(username), + Err(err) => Err(err), + } + } + + fn main() { + //fs::write("config.dat", "alice").unwrap(); + let username = read_username("config.dat"); + println!("username or error: {username:?}"); + } + +--------- +Details +--------- + +Simplify the :rust:`read_username` function to use :rust:`?`. + +Key points: + +- The :rust:`username` variable can be either :rust:`Ok(string)` or + :rust:`Err(error)`. +- Use the :rust:`fs::write` call to test out the different scenarios: no + file, empty file, file with username. +- Note that :rust:`main` can return a :rust:`Result<(), E>` as long as it + implements :rust:`std::process::Termination`. In practice, this means + that :rust:`E` implements :rust:`Debug`. The executable will print the + :rust:`Err` variant and return a nonzero exit status on error. diff --git a/courses/comprehensive_rust_training/200_error_handling/04_try_conversions.rst b/courses/comprehensive_rust_training/200_error_handling/04_try_conversions.rst new file mode 100644 index 000000000..cdba7b705 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/04_try_conversions.rst @@ -0,0 +1,98 @@ +================= +Try Conversions +================= + +----------------- +Try Conversions +----------------- + +The effective expansion of :rust:`?` is a little more complicated than +previously indicated: + +.. code:: rust + + expression? + +works the same as + +.. code:: rust + + match expression { + Ok(value) => value, + Err(err) => return Err(From::from(err)), + } + +The :rust:`From::from` call here means we attempt to convert the error type +to the type returned by the function. This makes it easy to encapsulate +errors into higher-level errors. + +--------- +Example +--------- + +.. code:: rust + + use std::error::Error; + use std::io::Read; + use std::{fmt, fs, io}; + + #[derive(Debug)] + enum ReadUsernameError { + IoError(io::Error), + EmptyUsername(String), + } + + impl Error for ReadUsernameError {} + + impl fmt::Display for ReadUsernameError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::IoError(e) => write!(f, "I/O error: {e}"), + Self::EmptyUsername(path) => write!(f, "Found no username in {path}"), + } + } + } + + impl From for ReadUsernameError { + fn from(err: io::Error) -> Self { + Self::IoError(err) + } + } + + fn read_username(path: &str) -> Result { + let mut username = String::with_capacity(100); + fs::File::open(path)?.read_to_string(&mut username)?; + if username.is_empty() { + return Err(ReadUsernameError::EmptyUsername(String::from(path))); + } + Ok(username) + } + + fn main() { + //std::fs::write("config.dat", "").unwrap(); + let username = read_username("config.dat"); + println!("username or error: {username:?}"); + } + +--------- +Details +--------- + +The :rust:`?` operator must return a value compatible with the return type +of the function. For :rust:`Result`, it means that the error types have to +be compatible. A function that returns :rust:`Result` can +only use :rust:`?` on a value of type :rust:`Result` if +:rust:`ErrorOuter` and :rust:`ErrorInner` are the same type or if :rust:`ErrorOuter` +implements :rust:`From`. + +A common alternative to a :rust:`From` implementation is +:rust:`Result::map_err`, especially when the conversion only happens in one +place. + +There is no compatibility requirement for :rust:`Option`. A function +returning :rust:`Option` can use the :rust:`?` operator on :rust:`Option` for +arbitrary :rust:`T` and :rust:`U` types. + +A function that returns :rust:`Result` cannot use :rust:`?` on :rust:`Option` and +vice versa. However, :rust:`Option::ok_or` converts :rust:`Option` to :rust:`Result` +whereas :rust:`Result::ok` turns :rust:`Result` into :rust:`Option`. diff --git a/courses/comprehensive_rust_training/200_error_handling/05_error.rst b/courses/comprehensive_rust_training/200_error_handling/05_error.rst new file mode 100644 index 000000000..7efb00f66 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/05_error.rst @@ -0,0 +1,49 @@ +===================== +Dynamic Error Types +===================== + +--------------------- +Dynamic Error Types +--------------------- + +Sometimes we want to allow any type of error to be returned without +writing our own enum covering all the different possibilities. The +:rust:`std::error::Error` trait makes it easy to create a trait object that +can contain any error. + +.. code:: rust + + use std::error::Error; + use std::fs; + use std::io::Read; + + fn read_count(path: &str) -> Result> { + let mut count_str = String::new(); + fs::File::open(path)?.read_to_string(&mut count_str)?; + let count: i32 = count_str.parse()?; + Ok(count) + } + + fn main() { + fs::write("count.dat", "1i3").unwrap(); + match read_count("count.dat") { + Ok(count) => println!("Count: {count}"), + Err(err) => println!("Error: {err}"), + } + } + +--------- +Details +--------- + +The :rust:`read_count` function can return :rust:`std::io::Error` (from file +operations) or :rust:`std::num::ParseIntError` (from :rust:`String::parse`). + +Boxing errors saves on code, but gives up the ability to cleanly handle +different error cases differently in the program. As such it's generally +not a good idea to use :rust:`Box` in the public API of a +library, but it can be a good option in a program where you just want to +display the error message somewhere. + +Make sure to implement the :rust:`std::error::Error` trait when defining a +custom error type so it can be boxed. diff --git a/courses/comprehensive_rust_training/200_error_handling/06_thiserror.rst b/courses/comprehensive_rust_training/200_error_handling/06_thiserror.rst new file mode 100644 index 000000000..57faa3115 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/06_thiserror.rst @@ -0,0 +1,55 @@ +=============== +"thiserror" +=============== + +--------------- +"thiserror" +--------------- + +The `thiserror `__ crate provides macros +to help avoid boilerplate when defining error types. It provides derive +macros that assist in implementing :rust:`From`, :rust:`Display`, and the +:rust:`Error` trait. + +.. code:: rust + + use std::io::Read; + use std::{fs, io}; + use thiserror::Error; + + #[derive(Debug, Error)] + enum ReadUsernameError { + #[error("I/O error: {0}")] + IoError(#[from] io::Error), + #[error("Found no username in {0}")] + EmptyUsername(String), + } + + fn read_username(path: &str) -> Result { + let mut username = String::with_capacity(100); + fs::File::open(path)?.read_to_string(&mut username)?; + if username.is_empty() { + return Err(ReadUsernameError::EmptyUsername(String::from(path))); + } + Ok(username) + } + + fn main() { + //fs::write("config.dat", "").unwrap(); + match read_username("config.dat") { + Ok(username) => println!("Username: {username}"), + Err(err) => println!("Error: {err:?}"), + } + } + +--------- +Details +--------- + +- The :rust:`Error` derive macro is provided by :rust:`thiserror`, and has lots + of useful attributes to help define error types in a compact way. +- The message from :rust:`#[error]` is used to derive the :rust:`Display` + trait. +- Note that the (:rust:`thiserror::`)\ :rust:`Error` derive macro, while it has + the effect of implementing the (:rust:`std::error::`)\ :rust:`Error` trait, + is not the same this; traits and macros do not share a namespace. diff --git a/courses/comprehensive_rust_training/200_error_handling/07_anyhow.rst b/courses/comprehensive_rust_training/200_error_handling/07_anyhow.rst new file mode 100644 index 000000000..8e947656d --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/07_anyhow.rst @@ -0,0 +1,78 @@ +============ +"anyhow" +============ + +------------ +"anyhow" +------------ + +The `anyhow `__ crate provides a rich error +type with support for carrying additional contextual information, which +can be used to provide a semantic trace of what the program was doing +leading up to the error. + +This can be combined with the convenience macros from +`thiserror `__ to avoid writing out +trait impls explicitly for custom error types. + +.. code:: rust + + use anyhow::{bail, Context, Result}; + use std::fs; + use std::io::Read; + use thiserror::Error; + + #[derive(Clone, Debug, Eq, Error, PartialEq)] + #[error("Found no username in {0}")] + struct EmptyUsernameError(String); + + fn read_username(path: &str) -> Result { + let mut username = String::with_capacity(100); + fs::File::open(path) + .with_context(|| format!("Failed to open {path}"))? + .read_to_string(&mut username) + .context("Failed to read")?; + if username.is_empty() { + bail!(EmptyUsernameError(path.to_string())); + } + Ok(username) + } + + fn main() { + //fs::write("config.dat", "").unwrap(); + match read_username("config.dat") { + Ok(username) => println!("Username: {username}"), + Err(err) => println!("Error: {err:?}"), + } + } + +--------- +Details +--------- + +- :rust:`anyhow::Error` is essentially a wrapper around :rust:`Box`. + As such it's again generally not a good choice for the public API of + a library, but is widely used in applications. +- :rust:`anyhow::Result` is a type alias for + :rust:`Result`. +- Functionality provided by :rust:`anyhow::Error` may be familiar to Go + developers, as it provides similar behavior to the Go :rust:`error` type + and :rust:`Result` is much like a Go :rust:`(T, error)` + (with the convention that only one element of the pair is + meaningful). +- :rust:`anyhow::Context` is a trait implemented for the standard + :rust:`Result` and :rust:`Option` types. :rust:`use anyhow::Context` is necessary + to enable :rust:`.context()` and :rust:`.with_context()` on those types. + +================= +More to Explore +================= + +----------------- +More to Explore +----------------- + +- :rust:`anyhow::Error` has support for downcasting, much like + :rust:`std::any::Any`; the specific error type stored inside can be + extracted for examination if desired with + `Error::downcast `__. diff --git a/courses/comprehensive_rust_training/200_error_handling/08_exercise.rst b/courses/comprehensive_rust_training/200_error_handling/08_exercise.rst new file mode 100644 index 000000000..00b0abac8 --- /dev/null +++ b/courses/comprehensive_rust_training/200_error_handling/08_exercise.rst @@ -0,0 +1,27 @@ +================================= +Exercise: Rewriting with Result +================================= + +--------------------------------- +Exercise: Rewriting with Result +--------------------------------- + +In this exercise we're revisiting the expression evaluator exercise that +we did in day 2. Our initial solution ignores a possible error case: +Dividing by zero! Rewrite :rust:`eval` to instead use idiomatic error +handling to handle this error case and return an error when it occurs. +We provide a simple :rust:`DivideByZeroError` type to use as the error type +for :rust:`eval`. + +.. code:: rust + + {{#include exercise.rs:types}} + + {{#include exercise.rs:eval}} + + {{#include exercise.rs:tests}} + +- The starting code here isn't exactly the same as the previous + exercise's solution: We've added in an explicit panic to show + students where the error case is. Point this out if students get + confused. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust.rst b/courses/comprehensive_rust_training/210_unsafe_rust.rst new file mode 100644 index 000000000..5dcfeb734 --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust.rst @@ -0,0 +1,45 @@ +************* +Unsafe Rust +************* + +.. container:: PRELUDE BEGIN + +.. container:: PRELUDE ROLES + +.. role:: ada(code) + :language: Ada + +.. role:: C(code) + :language: C + +.. role:: cpp(code) + :language: C++ + +.. role:: rust(code) + :language: Rust + +.. container:: PRELUDE SYMBOLS + +.. |rightarrow| replace:: :math:`\rightarrow` +.. |forall| replace:: :math:`\forall` +.. |exists| replace:: :math:`\exists` +.. |equivalent| replace:: :math:`\iff` +.. |le| replace:: :math:`\le` +.. |ge| replace:: :math:`\ge` +.. |lt| replace:: :math:`<` +.. |gt| replace:: :math:`>` +.. |checkmark| replace:: :math:`\checkmark` + +.. container:: PRELUDE REQUIRES + +.. container:: PRELUDE PROVIDES + +.. container:: PRELUDE END + +.. include:: 210_unsafe_rust/01_unsafe.rst +.. include:: 210_unsafe_rust/02_dereferencing.rst +.. include:: 210_unsafe_rust/03_mutable_static.rst +.. include:: 210_unsafe_rust/04_unions.rst +.. include:: 210_unsafe_rust/05_unsafe_functions.rst +.. include:: 210_unsafe_rust/06_unsafe_traits.rst +.. include:: 210_unsafe_rust/07_exercise.rst diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/01_unsafe.rst b/courses/comprehensive_rust_training/210_unsafe_rust/01_unsafe.rst new file mode 100644 index 000000000..4c8b58885 --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/01_unsafe.rst @@ -0,0 +1,41 @@ +============= +Unsafe Rust +============= + +------------- +Unsafe Rust +------------- + +The Rust language has two parts: + +- **Safe Rust:** memory safe, no undefined behavior possible. +- **Unsafe Rust:** can trigger undefined behavior if preconditions are + violated. + +We saw mostly safe Rust in this course, but it's important to know what +Unsafe Rust is. + +Unsafe code is usually small and isolated, and its correctness should be +carefully documented. It is usually wrapped in a safe abstraction layer. + +Unsafe Rust gives you access to five new capabilities: + +- Dereference raw pointers. +- Access or modify mutable static variables. +- Access :rust:`union` fields. +- Call :rust:`unsafe` functions, including :rust:`extern` functions. +- Implement :rust:`unsafe` traits. + +We will briefly cover unsafe capabilities next. For full details, please +see +`Chapter 19.1 in the Rust Book `__ and +the `Rustonomicon `__. + +--------- +Details +--------- + +Unsafe Rust does not mean the code is incorrect. It means that +developers have turned off some compiler safety features and have to +write correct code by themselves. It means the compiler no longer +enforces Rust's memory-safety rules. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/02_dereferencing.rst b/courses/comprehensive_rust_training/210_unsafe_rust/02_dereferencing.rst new file mode 100644 index 000000000..5958a9f45 --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/02_dereferencing.rst @@ -0,0 +1,65 @@ +============================ +Dereferencing Raw Pointers +============================ + +---------------------------- +Dereferencing Raw Pointers +---------------------------- + +Creating pointers is safe, but dereferencing them requires :rust:`unsafe`: + +.. code:: rust + + fn main() { + let mut s = String::from("careful!"); + + let r1 = &raw mut s; + let r2 = r1 as *const String; + + // SAFETY: r1 and r2 were obtained from references and so are guaranteed to + // be non-null and properly aligned, the objects underlying the references + // from which they were obtained are live throughout the whole unsafe + // block, and they are not accessed either through the references or + // concurrently through any other pointers. + unsafe { + println!("r1 is: {}", *r1); + *r1 = String::from("uhoh"); + println!("r2 is: {}", *r2); + } + + // NOT SAFE. DO NOT DO THIS. + /* + let r3: &String = unsafe { &*r1 }; + drop(s); + println!("r3 is: {}", *r3); + */ + } + +--------- +Details +--------- + +It is good practice (and required by the Android Rust style guide) to +write a comment for each :rust:`unsafe` block explaining how the code inside +it satisfies the safety requirements of the unsafe operations it is +doing. + +In the case of pointer dereferences, this means that the pointers must +be `valid `__, +i.e.: + +- The pointer must be non-null. +- The pointer must be *dereferenceable* (within the bounds of a single + allocated object). +- The object must not have been deallocated. +- There must not be concurrent accesses to the same location. +- If the pointer was obtained by casting a reference, the underlying + object must be live and no reference may be used to access the + memory. + +In most cases the pointer must also be properly aligned. + +The "NOT SAFE" section gives an example of a common kind of UB bug: +:rust:`*r1` has the :rust:`'static` lifetime, so :rust:`r3` has type +:rust:`&'static String`, and thus outlives :rust:`s`. Creating a reference from +a pointer requires *great care*. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/03_mutable_static.rst b/courses/comprehensive_rust_training/210_unsafe_rust/03_mutable_static.rst new file mode 100644 index 000000000..5b7f2376f --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/03_mutable_static.rst @@ -0,0 +1,53 @@ +========================== +Mutable Static Variables +========================== + +-------------------------- +Mutable Static Variables +-------------------------- + +It is safe to read an immutable static variable: + +.. code:: rust + + static HELLO_WORLD: &str = "Hello, world!"; + + fn main() { + println!("HELLO_WORLD: {HELLO_WORLD}"); + } + +However, since data races can occur, it is unsafe to read and write +mutable static variables: + +.. code:: rust + + static mut COUNTER: u32 = 0; + + fn add_to_counter(inc: u32) { + // SAFETY: There are no other threads which could be accessing `COUNTER`. + unsafe { + COUNTER += inc; + } + } + + fn main() { + add_to_counter(42); + + // SAFETY: There are no other threads which could be accessing `COUNTER`. + unsafe { + println!("COUNTER: {COUNTER}"); + } + } + +--------- +Details +--------- + +- The program here is safe because it is single-threaded. However, the + Rust compiler is conservative and will assume the worst. Try removing + the :rust:`unsafe` and see how the compiler explains that it is undefined + behavior to mutate a static from multiple threads. + +- Using a mutable static is generally a bad idea, but there are some + cases where it might make sense in low-level :rust:`no_std` code, such as + implementing a heap allocator or working with some C APIs. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/04_unions.rst b/courses/comprehensive_rust_training/210_unsafe_rust/04_unions.rst new file mode 100644 index 000000000..b18d3de2a --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/04_unions.rst @@ -0,0 +1,36 @@ +======== +Unions +======== + +-------- +Unions +-------- + +Unions are like enums, but you need to track the active field yourself: + +.. code:: rust + + #[repr(C)] + union MyUnion { + i: u8, + b: bool, + } + + fn main() { + let u = MyUnion { i: 42 }; + println!("int: {}", unsafe { u.i }); + println!("bool: {}", unsafe { u.b }); // Undefined behavior! + } + +--------- +Details +--------- + +Unions are very rarely needed in Rust as you can usually use an enum. +They are occasionally needed for interacting with C library APIs. + +If you just want to reinterpret bytes as a different type, you probably +want +`std::mem::transmute `__ +or a safe wrapper such as the +`zerocopy `__ crate. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/05_unsafe_functions.rst b/courses/comprehensive_rust_training/210_unsafe_rust/05_unsafe_functions.rst new file mode 100644 index 000000000..6e67211af --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/05_unsafe_functions.rst @@ -0,0 +1,112 @@ +================== +Unsafe Functions +================== + +-------------------------- +Calling Unsafe Functions +-------------------------- + +A function or method can be marked :rust:`unsafe` if it has extra +preconditions you must uphold to avoid undefined behaviour: + +.. code:: rust + + extern "C" { + fn abs(input: i32) -> i32; + } + + fn main() { + let emojis = "TBDTBDTBD"; + + // SAFETY: The indices are in the correct order, within the bounds of the + // string slice, and lie on UTF-8 sequence boundaries. + unsafe { + println!("emoji: {}", emojis.get_unchecked(0..4)); + println!("emoji: {}", emojis.get_unchecked(4..7)); + println!("emoji: {}", emojis.get_unchecked(7..11)); + } + + println!("char count: {}", count_chars(unsafe { emojis.get_unchecked(0..7) })); + + // SAFETY: `abs` doesn't deal with pointers and doesn't have any safety + // requirements. + unsafe { + println!("Absolute value of -3 according to C: {}", abs(-3)); + } + + // Not upholding the UTF-8 encoding requirement breaks memory safety! + // println!("emoji: {}", unsafe { emojis.get_unchecked(0..3) }); + // println!("char count: {}", count_chars(unsafe { + // emojis.get_unchecked(0..3) })); + } + + fn count_chars(s: &str) -> usize { + s.chars().count() + } + +-------------------------- +Writing Unsafe Functions +-------------------------- + +You can mark your own functions as :rust:`unsafe` if they require particular +conditions to avoid undefined behaviour. + +.. code:: rust + + /// Swaps the values pointed to by the given pointers. + /// + /// # Safety + /// + /// The pointers must be valid and properly aligned. + unsafe fn swap(a: *mut u8, b: *mut u8) { + let temp = *a; + *a = *b; + *b = temp; + } + + fn main() { + let mut a = 42; + let mut b = 66; + + // SAFETY: ... + unsafe { + swap(&mut a, &mut b); + } + + println!("a = {}, b = {}", a, b); + } + +--------- +Details +--------- + +.. _calling-unsafe-functions-1: + +-------------------------- +Calling Unsafe Functions +-------------------------- + +:rust:`get_unchecked`, like most :rust:`_unchecked` functions, is unsafe, +because it can create UB if the range is incorrect. :rust:`abs` is unsafe +for a different reason: it is an external function (FFI). Calling +external functions is usually only a problem when those functions do +things with pointers which might violate Rust's memory model, but in +general any C function might have undefined behaviour under any +arbitrary circumstances. + +The :rust:`"C"` in this example is the ABI; +`other ABIs are available too `__. + +.. _writing-unsafe-functions-1: + +-------------------------- +Writing Unsafe Functions +-------------------------- + +We wouldn't actually use pointers for a :rust:`swap` function - it can be +done safely with references. + +Note that unsafe code is allowed within an unsafe function without an +:rust:`unsafe` block. We can prohibit this with +:rust:`#[deny(unsafe_op_in_unsafe_fn)]`. Try adding it and see what happens. +This will likely change in a future Rust edition. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/06_unsafe_traits.rst b/courses/comprehensive_rust_training/210_unsafe_rust/06_unsafe_traits.rst new file mode 100644 index 000000000..75e90d129 --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/06_unsafe_traits.rst @@ -0,0 +1,44 @@ +============================ +Implementing Unsafe Traits +============================ + +---------------------------- +Implementing Unsafe Traits +---------------------------- + +Like with functions, you can mark a trait as :rust:`unsafe` if the +implementation must guarantee particular conditions to avoid undefined +behaviour. + +For example, the :rust:`zerocopy` crate has an unsafe trait that looks +`something like this `__: + +.. code:: rust + + use std::{mem, slice}; + + /// ... + /// # Safety + /// The type must have a defined representation and no padding. + pub unsafe trait IntoBytes { + fn as_bytes(&self) -> &[u8] { + let len = mem::size_of_val(self); + let slf: *const Self = self; + unsafe { slice::from_raw_parts(slf.cast::(), len) } + } + } + + // SAFETY: `u32` has a defined representation and no padding. + unsafe impl IntoBytes for u32 {} + +--------- +Details +--------- + +There should be a :rust:`# Safety` section on the Rustdoc for the trait +explaining the requirements for the trait to be safely implemented. + +The actual safety section for :rust:`IntoBytes` is rather longer and more +complicated. + +The built-in :rust:`Send` and :rust:`Sync` traits are unsafe. diff --git a/courses/comprehensive_rust_training/210_unsafe_rust/07_exercise.rst b/courses/comprehensive_rust_training/210_unsafe_rust/07_exercise.rst new file mode 100644 index 000000000..69b9082e4 --- /dev/null +++ b/courses/comprehensive_rust_training/210_unsafe_rust/07_exercise.rst @@ -0,0 +1,87 @@ +================== +Safe FFI Wrapper +================== + +------------------ +Safe FFI Wrapper +------------------ + +Rust has great support for calling functions through a *foreign function +interface* (FFI). We will use this to build a safe wrapper for the +:rust:`libc` functions you would use from C to read the names of files in a +directory. + +You will want to consult the manual pages: + +- `opendir(3) `__ +- `readdir(3) `__ +- `closedir(3) `__ + +You will also want to browse the +`std::ffi `__ module. There you +find a number of string types which you need for the exercise: + +.. list-table:: + :header-rows: 1 + + * - Types + - Encoding + - Use + + * - `str `__ and `String `__ + - UTF-8 + - Text processing in Rust + + * - `CStr `__ and `CString `__ + - NUL-terminated + - Communicating with C functions + + * - `OsStr `__ and `OsString `__ + - OS-specific + - Communicating with the OS + +You will convert between all these types: + +- :rust:`&str` to :rust:`CString`: you need to allocate space for a trailing + :rust:`\0` character, +- :rust:`CString` to :rust:`*const i8`: you need a pointer to call C functions, +- :rust:`*const i8` to :rust:`&CStr`: you need something which can find the + trailing :rust:`\0` character, +- :rust:`&CStr` to :rust:`&[u8]`: a slice of bytes is the universal interface + for "some unknown data", +- :rust:`&[u8]` to :rust:`&OsStr`: :rust:`&OsStr` is a step towards :rust:`OsString`, + use + `OsStrExt `__ + to create it, +- :rust:`&OsStr` to :rust:`OsString`: you need to clone the data in :rust:`&OsStr` + to be able to return it and call :rust:`readdir` again. + +The `Nomicon `__ also has a +very useful chapter about FFI. + +Copy the code below to https://play.rust-lang.org/ and fill in the +missing functions and methods: + +.. code:: rust + + // TODO: remove this when you're done with your implementation. + #![allow(unused_imports, unused_variables, dead_code)] + + {{#include exercise.rs:ffi}} + + {{#include exercise.rs:DirectoryIterator}} + unimplemented!() + } + } + + {{#include exercise.rs:Iterator}} + unimplemented!() + } + } + + {{#include exercise.rs:Drop}} + unimplemented!() + } + } + + {{#include exercise.rs:main}} diff --git a/courses/comprehensive_rust_training/comprehensive_course.txt b/courses/comprehensive_rust_training/comprehensive_course.txt new file mode 100644 index 000000000..f53aca18c --- /dev/null +++ b/courses/comprehensive_rust_training/comprehensive_course.txt @@ -0,0 +1,21 @@ +010_introduction.rst +020_hello_world.rst +030_types_and_values.rst +040_control_flow_basics.rst +050_tuples_and_arrays.rst +060_references.rst +070_user_defined_types.rst +080_pattern_matching.rst +090_methods_and_traits.rst +100_generics.rst +110_std_types.rst +120_std_traits.rst +130_memory_management.rst +140_smart_pointers.rst +150_borrowing.rst +160_lifetimes.rst +170_iterators.rst +180_modules.rst +190_testing.rst +200_error_handling.rst +210_unsafe_rust.rst diff --git a/courses/comprehensive_rust_training/course.toml b/courses/comprehensive_rust_training/course.toml new file mode 100644 index 000000000..6da1dc1c2 --- /dev/null +++ b/courses/comprehensive_rust_training/course.toml @@ -0,0 +1 @@ +name = "Comprehensive Rust Training" diff --git a/courses/comprehensive_rust_training/standard_course.txt b/courses/comprehensive_rust_training/standard_course.txt new file mode 100644 index 000000000..83fb911a2 --- /dev/null +++ b/courses/comprehensive_rust_training/standard_course.txt @@ -0,0 +1,19 @@ +010_introduction.rst +020_hello_world.rst +030_types_and_values.rst +040_control_flow_basics.rst +050_tuples_and_arrays.rst +060_references.rst +070_user_defined_types.rst +080_pattern_matching.rst +090_methods_and_traits.rst +100_generics.rst +110_std_types.rst +120_std_traits.rst +130_memory_management.rst +140_smart_pointers.rst +150_borrowing.rst +160_lifetimes.rst +170_iterators.rst +180_modules.rst +200_error_handling.rst diff --git a/courses/fundamentals_of_ada/005_introduction.rst b/courses/fundamentals_of_ada/005_introduction.rst index a651ec316..361aa1c22 100644 --- a/courses/fundamentals_of_ada/005_introduction.rst +++ b/courses/fundamentals_of_ada/005_introduction.rst @@ -15,6 +15,9 @@ Introduction .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/010_overview.rst b/courses/fundamentals_of_ada/010_overview.rst index 6d0d16484..3f9a281e0 100644 --- a/courses/fundamentals_of_ada/010_overview.rst +++ b/courses/fundamentals_of_ada/010_overview.rst @@ -15,6 +15,9 @@ Overview .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/020_declarations.rst b/courses/fundamentals_of_ada/020_declarations.rst index c7634caec..4e5904e1b 100644 --- a/courses/fundamentals_of_ada/020_declarations.rst +++ b/courses/fundamentals_of_ada/020_declarations.rst @@ -15,6 +15,9 @@ Declarations .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/030_basic_types-extras.rst b/courses/fundamentals_of_ada/030_basic_types-extras.rst index 9859c5b99..10ce21a95 100644 --- a/courses/fundamentals_of_ada/030_basic_types-extras.rst +++ b/courses/fundamentals_of_ada/030_basic_types-extras.rst @@ -15,6 +15,9 @@ Basic Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/030_basic_types-in_depth.rst b/courses/fundamentals_of_ada/030_basic_types-in_depth.rst index e6a6f3464..3ca6d8a38 100644 --- a/courses/fundamentals_of_ada/030_basic_types-in_depth.rst +++ b/courses/fundamentals_of_ada/030_basic_types-in_depth.rst @@ -15,6 +15,9 @@ Basic Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/030_basic_types.rst b/courses/fundamentals_of_ada/030_basic_types.rst index 8345f2f8a..e86f23f97 100644 --- a/courses/fundamentals_of_ada/030_basic_types.rst +++ b/courses/fundamentals_of_ada/030_basic_types.rst @@ -15,6 +15,9 @@ Basic Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/040_statements.rst b/courses/fundamentals_of_ada/040_statements.rst index cef3fc4db..c717af490 100644 --- a/courses/fundamentals_of_ada/040_statements.rst +++ b/courses/fundamentals_of_ada/040_statements.rst @@ -15,6 +15,9 @@ Statements .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/050_array_types.rst b/courses/fundamentals_of_ada/050_array_types.rst index 3149cf687..85eefe516 100644 --- a/courses/fundamentals_of_ada/050_array_types.rst +++ b/courses/fundamentals_of_ada/050_array_types.rst @@ -15,6 +15,9 @@ Array Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/060_record_types.rst b/courses/fundamentals_of_ada/060_record_types.rst index 064023c4c..a0d38502c 100644 --- a/courses/fundamentals_of_ada/060_record_types.rst +++ b/courses/fundamentals_of_ada/060_record_types.rst @@ -15,6 +15,9 @@ Record Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/065_discriminated_records.rst b/courses/fundamentals_of_ada/065_discriminated_records.rst index a74ae182e..ba093f97b 100644 --- a/courses/fundamentals_of_ada/065_discriminated_records.rst +++ b/courses/fundamentals_of_ada/065_discriminated_records.rst @@ -15,6 +15,9 @@ Discriminated Records .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/070_subprograms.rst b/courses/fundamentals_of_ada/070_subprograms.rst index 76f729c3a..ba4a21da4 100644 --- a/courses/fundamentals_of_ada/070_subprograms.rst +++ b/courses/fundamentals_of_ada/070_subprograms.rst @@ -15,6 +15,9 @@ Subprograms .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/075_type_derivation.rst b/courses/fundamentals_of_ada/075_type_derivation.rst index bb08dbf58..8986d9df7 100644 --- a/courses/fundamentals_of_ada/075_type_derivation.rst +++ b/courses/fundamentals_of_ada/075_type_derivation.rst @@ -15,6 +15,9 @@ Type Derivation .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/080_expressions-with_quantified.rst b/courses/fundamentals_of_ada/080_expressions-with_quantified.rst index 57db31053..7fc71c7d2 100644 --- a/courses/fundamentals_of_ada/080_expressions-with_quantified.rst +++ b/courses/fundamentals_of_ada/080_expressions-with_quantified.rst @@ -15,6 +15,9 @@ Expressions .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/080_expressions.rst b/courses/fundamentals_of_ada/080_expressions.rst index 09657d1c1..6392ec597 100644 --- a/courses/fundamentals_of_ada/080_expressions.rst +++ b/courses/fundamentals_of_ada/080_expressions.rst @@ -15,6 +15,9 @@ Expressions .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/090_overloading-with_equality.rst b/courses/fundamentals_of_ada/090_overloading-with_equality.rst index 2d200ed66..b4683022a 100644 --- a/courses/fundamentals_of_ada/090_overloading-with_equality.rst +++ b/courses/fundamentals_of_ada/090_overloading-with_equality.rst @@ -15,6 +15,9 @@ Overloading .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/090_overloading.rst b/courses/fundamentals_of_ada/090_overloading.rst index 276631483..c9fe30fdf 100644 --- a/courses/fundamentals_of_ada/090_overloading.rst +++ b/courses/fundamentals_of_ada/090_overloading.rst @@ -15,6 +15,9 @@ Overloading .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/100_packages.rst b/courses/fundamentals_of_ada/100_packages.rst index 0cb79017c..cafbc77e8 100644 --- a/courses/fundamentals_of_ada/100_packages.rst +++ b/courses/fundamentals_of_ada/100_packages.rst @@ -15,6 +15,9 @@ Packages .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/110_private_types.rst b/courses/fundamentals_of_ada/110_private_types.rst index 0dbebe49e..5c3a56a78 100644 --- a/courses/fundamentals_of_ada/110_private_types.rst +++ b/courses/fundamentals_of_ada/110_private_types.rst @@ -15,6 +15,9 @@ Private Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/120_limited_types.rst b/courses/fundamentals_of_ada/120_limited_types.rst index 90ebb0c7c..367c10afb 100644 --- a/courses/fundamentals_of_ada/120_limited_types.rst +++ b/courses/fundamentals_of_ada/120_limited_types.rst @@ -15,6 +15,9 @@ Limited Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/130_program_structure.rst b/courses/fundamentals_of_ada/130_program_structure.rst index 07d02d65e..7bcf276ae 100644 --- a/courses/fundamentals_of_ada/130_program_structure.rst +++ b/courses/fundamentals_of_ada/130_program_structure.rst @@ -15,6 +15,9 @@ Program Structure .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/130_program_structure/04-hierarchical_library_units.rst b/courses/fundamentals_of_ada/130_program_structure/04-hierarchical_library_units.rst index ca2185085..6192b8758 100644 --- a/courses/fundamentals_of_ada/130_program_structure/04-hierarchical_library_units.rst +++ b/courses/fundamentals_of_ada/130_program_structure/04-hierarchical_library_units.rst @@ -26,10 +26,6 @@ Problem: Packages Are Not Enough Solution: Hierarchical Library Units -------------------------------------- -.. container:: columns - - .. container:: column - * Address extensibility issue - Can extend packages with visibility to parent private part @@ -40,9 +36,33 @@ Solution: Hierarchical Library Units - Extensions all have the same ancestor *root* name - .. container:: column +------------------------------- +Visibility Across a Hierarchy +------------------------------- + +.. container:: overlay 1 + + .. image:: hierarchical_visibility_1.svg + :width: 70% + :align: center + +.. container:: overlay 2 + + .. image:: hierarchical_visibility_2.svg + :width: 70% + :align: center + +.. container:: overlay 3 + + .. image:: hierarchical_visibility_3.svg + :width: 70% + :align: center + +.. container:: overlay 4 - .. image:: hierarchical_library_units.png + .. image:: hierarchical_visibility_4.svg + :width: 70% + :align: center -------------------------- Programming by Extension diff --git a/courses/fundamentals_of_ada/135_visibility.rst b/courses/fundamentals_of_ada/135_visibility.rst index 104b32f78..62b50f375 100644 --- a/courses/fundamentals_of_ada/135_visibility.rst +++ b/courses/fundamentals_of_ada/135_visibility.rst @@ -15,6 +15,9 @@ Visibility .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/140_access_types-in_depth.rst b/courses/fundamentals_of_ada/140_access_types-in_depth.rst index a5b8042fa..87b5de62b 100644 --- a/courses/fundamentals_of_ada/140_access_types-in_depth.rst +++ b/courses/fundamentals_of_ada/140_access_types-in_depth.rst @@ -15,6 +15,9 @@ Access Types In Depth .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/140_access_types.rst b/courses/fundamentals_of_ada/140_access_types.rst index b7b314953..bf88d7922 100644 --- a/courses/fundamentals_of_ada/140_access_types.rst +++ b/courses/fundamentals_of_ada/140_access_types.rst @@ -15,6 +15,9 @@ Access Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/160_genericity-intro.rst b/courses/fundamentals_of_ada/160_genericity-intro.rst index 33f89e91c..c1a11ada3 100644 --- a/courses/fundamentals_of_ada/160_genericity-intro.rst +++ b/courses/fundamentals_of_ada/160_genericity-intro.rst @@ -15,6 +15,9 @@ Genericity .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/160_genericity.rst b/courses/fundamentals_of_ada/160_genericity.rst index 407f4a2f2..e3242e178 100644 --- a/courses/fundamentals_of_ada/160_genericity.rst +++ b/courses/fundamentals_of_ada/160_genericity.rst @@ -15,6 +15,9 @@ Genericity .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/170_tagged_derivation-intro.rst b/courses/fundamentals_of_ada/170_tagged_derivation-intro.rst index 8e334a127..647a5f355 100644 --- a/courses/fundamentals_of_ada/170_tagged_derivation-intro.rst +++ b/courses/fundamentals_of_ada/170_tagged_derivation-intro.rst @@ -15,6 +15,9 @@ Tagged Derivation .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/170_tagged_derivation.rst b/courses/fundamentals_of_ada/170_tagged_derivation.rst index cd19f2053..7aafdd731 100644 --- a/courses/fundamentals_of_ada/170_tagged_derivation.rst +++ b/courses/fundamentals_of_ada/170_tagged_derivation.rst @@ -15,6 +15,9 @@ Tagged Derivation .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/175_multiple_inheritance.rst b/courses/fundamentals_of_ada/175_multiple_inheritance.rst index 60b70a15b..dc08e7778 100644 --- a/courses/fundamentals_of_ada/175_multiple_inheritance.rst +++ b/courses/fundamentals_of_ada/175_multiple_inheritance.rst @@ -15,6 +15,9 @@ Multiple Inheritance .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/180_polymorphism.rst b/courses/fundamentals_of_ada/180_polymorphism.rst index a353ec641..bedbf5a43 100644 --- a/courses/fundamentals_of_ada/180_polymorphism.rst +++ b/courses/fundamentals_of_ada/180_polymorphism.rst @@ -15,6 +15,9 @@ Polymorphism .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/190_exceptions-in_depth.rst b/courses/fundamentals_of_ada/190_exceptions-in_depth.rst index b24a5b89c..b93235e6e 100644 --- a/courses/fundamentals_of_ada/190_exceptions-in_depth.rst +++ b/courses/fundamentals_of_ada/190_exceptions-in_depth.rst @@ -15,6 +15,9 @@ Exceptions In-Depth .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/190_exceptions.rst b/courses/fundamentals_of_ada/190_exceptions.rst index b0c7fe408..b328c6148 100644 --- a/courses/fundamentals_of_ada/190_exceptions.rst +++ b/courses/fundamentals_of_ada/190_exceptions.rst @@ -15,6 +15,9 @@ Exceptions .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/230_interfacing_with_c.rst b/courses/fundamentals_of_ada/230_interfacing_with_c.rst index 692d617ea..0e98c5f77 100644 --- a/courses/fundamentals_of_ada/230_interfacing_with_c.rst +++ b/courses/fundamentals_of_ada/230_interfacing_with_c.rst @@ -15,6 +15,9 @@ Interfacing with C .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/240_tasking_in_depth.rst b/courses/fundamentals_of_ada/240_tasking-in_depth.rst similarity index 96% rename from courses/fundamentals_of_ada/240_tasking_in_depth.rst rename to courses/fundamentals_of_ada/240_tasking-in_depth.rst index 82b091859..52dd3c42d 100644 --- a/courses/fundamentals_of_ada/240_tasking_in_depth.rst +++ b/courses/fundamentals_of_ada/240_tasking-in_depth.rst @@ -15,6 +15,9 @@ Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/240_tasking_light.rst b/courses/fundamentals_of_ada/240_tasking-light.rst similarity index 96% rename from courses/fundamentals_of_ada/240_tasking_light.rst rename to courses/fundamentals_of_ada/240_tasking-light.rst index 326eb2e92..b3dd578e4 100644 --- a/courses/fundamentals_of_ada/240_tasking_light.rst +++ b/courses/fundamentals_of_ada/240_tasking-light.rst @@ -15,6 +15,9 @@ Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/240_tasking_light_in_depth.rst b/courses/fundamentals_of_ada/240_tasking-light_in_depth.rst similarity index 97% rename from courses/fundamentals_of_ada/240_tasking_light_in_depth.rst rename to courses/fundamentals_of_ada/240_tasking-light_in_depth.rst index c9f7fbaea..35b4be10b 100644 --- a/courses/fundamentals_of_ada/240_tasking_light_in_depth.rst +++ b/courses/fundamentals_of_ada/240_tasking-light_in_depth.rst @@ -15,6 +15,9 @@ Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/240_tasking.rst b/courses/fundamentals_of_ada/240_tasking.rst index 1f575f703..fc005b425 100644 --- a/courses/fundamentals_of_ada/240_tasking.rst +++ b/courses/fundamentals_of_ada/240_tasking.rst @@ -15,6 +15,9 @@ Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/245_ravenscar_tasking-with_tasking_overview.rst b/courses/fundamentals_of_ada/245_ravenscar_tasking-with_tasking_overview.rst index 8799903f8..597098a96 100644 --- a/courses/fundamentals_of_ada/245_ravenscar_tasking-with_tasking_overview.rst +++ b/courses/fundamentals_of_ada/245_ravenscar_tasking-with_tasking_overview.rst @@ -15,6 +15,9 @@ Ravenscar Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/245_ravenscar_tasking.rst b/courses/fundamentals_of_ada/245_ravenscar_tasking.rst index 1172f48ca..0160ef0e4 100644 --- a/courses/fundamentals_of_ada/245_ravenscar_tasking.rst +++ b/courses/fundamentals_of_ada/245_ravenscar_tasking.rst @@ -15,6 +15,9 @@ Ravenscar Tasking .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/260_controlled_types.rst b/courses/fundamentals_of_ada/260_controlled_types.rst index e65b49b63..8954d42ff 100644 --- a/courses/fundamentals_of_ada/260_controlled_types.rst +++ b/courses/fundamentals_of_ada/260_controlled_types.rst @@ -15,6 +15,9 @@ Controlled Types .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` @@ -33,249 +36,8 @@ Controlled Types .. container:: PRELUDE END -============== -Introduction -============== - -------------------------- -Constructor / Destructor -------------------------- - -* Possible to specify behavior of object initialization, finalization, and assignment - - - Based on type definition - - Type must derive from `Controlled` or `Limited_Controlled` in package `Ada.Finalization` - -* This derived type is called a *controlled type* - - - User may override any or all subprograms in `Ada.Finalization` - - Default implementation is a null body - -================== -Ada.Finalization -================== - ---------------- -Package Spec ---------------- - -.. code:: Ada - - package Ada.Finalization is - - type Controlled is abstract tagged private; - procedure Initialize (Object : in out Controlled) - is null; - procedure Adjust (Object : in out Controlled) - is null; - procedure Finalize (Object : in out Controlled) - is null; - - type Limited_Controlled is abstract tagged limited private; - procedure Initialize (Object : in out Limited_Controlled) - is null; - procedure Finalize (Object : in out Limited_Controlled) - is null; - - private - -- implementation defined - end Ada.Finalization; - -------- -Uses -------- - -* Prevent "resource leak" - - - Logic centralized in service rather than distributed across clients - -* Examples: heap reclamation, "mutex" unlocking -* User-defined assignment - ----------------- -Initialization ----------------- - -* Subprogram `Initialize` invoked after object created - - - Either by object declaration or allocator - - Only if no explicit initialization expression - -* Often default initialization expressions on record components are sufficient - - - No need for an explicit call to `Initialize` - -* Similar to C++ constructor - ----------------- -Finalization ----------------- - -* Subprogram `Finalize` invoked just before object is destroyed - - - Leaving the scope of a declared object - - Unchecked deallocation of an allocated object - -* Similar to C++ destructor - ------------- -Assignment ------------- - -* Subprogram `Adjust` invoked as part of an assignment operation -* Assignment statement `Target := Source;` is basically: - - - `Finalize (Target)` - - Copy Source to Target - - `Adjust (Target)` - - *Actual rules are more complicated, e.g. to allow cases where Target and Source are the same object* - -* Typical situations where objects are access values - - - `Finalize` does unchecked deallocation or decrements a reference count - - The copy step copies the access value - - `Adjust` either clones a "deep copy" of the referenced object or increments a reference count - -========= -Example -========= - ----------------------------------- -Unbounded String Via Access Type ----------------------------------- - -* Type contains a pointer to a string type -* We want the provider to allocate and free memory "safely" - - - No sharing - - `Adjust` allocates referenced String - - `Finalize` frees the referenced String - - Assignment deallocates target string and assigns copy of source string to target string - ------------------------- -Unbounded String Usage ------------------------- - -.. code:: Ada - - with Unbounded_String_Pkg; use Unbounded_String_Pkg; - procedure Test is - U1 : Ustring_T; - begin - U1 := To_Ustring_T ("Hello"); - declare - U2 : Ustring_T; - begin - U2 := To_Ustring_T ("Goodbye"); - U1 := U2; -- Reclaims U1 memory - end; -- Reclaims U2 memory - end Test; -- Reclaims U1 memory - ------------------------------ -Unbounded String Definition ------------------------------ - -.. code:: Ada - - with Ada.Finalization; use Ada.Finalization; - package Unbounded_String_Pkg is - -- Implement unbounded strings - type Ustring_T is private; - function "=" (L, R : Ustring_T) return Boolean; - function To_Ustring_T (Item : String) return Ustring_T; - function To_String (Item : Ustring_T) return String; - function Length (Item : Ustring_T) return Natural; - function "&" (L, R : Ustring_T) return Ustring_T; - private - type String_Ref is access String; - type Ustring_T is new Controlled with record - Ref : String_Ref := new String (1 .. 0); - end record; - procedure Finalize (Object : in out Ustring_T); - procedure Adjust (Object : in out Ustring_T); - end Unbounded_String_Pkg; - ---------------------------------- -Unbounded String Implementation ---------------------------------- - -.. code:: Ada - - with Ada.Unchecked_Deallocation; - package body Unbounded_String_Pkg is - procedure Free_String is new Ada.Unchecked_Deallocation - (String, String_Ref); - - function "=" (L, R : Ustring_T) return Boolean is - (L.Ref.all = R.Ref.all); - - function To_Ustring_T (Item : String) return Ustring_T is - (Controlled with Ref => new String'(Item)); - - function To_String (Item : Ustring_T) return String is - (Item.Ref.all); - - function Length (Item : Ustring_T) return Natural is - (Item.Ref.all'Length); - - function "&" (L, R : Ustring_T) return Ustring_T is - (Controlled with Ref => new String'(L.Ref.all & R.Ref.all); - - procedure Finalize (Object : in out Ustring_T) is - begin - Free_String (Object.Ref); - end Finalize; - - procedure Adjust (Object : in out Ustring_T) is - begin - Object.Ref := new String'(Object.Ref.all); - end Adjust; - end Unbounded_String_Pkg; - ------------------- -Finalizable Aspect ------------------- - -* Uses the GNAT-specific :ada:`with Finalizable` aspect - -.. code:: Ada - - type Ctrl is record - Id : Natural := 0; - end record - with Finalizable => (Initialize => Initialize, - Adjust => Adjust, - Finalize => Finalize, - Relaxed_Finalization => True); - - procedure Adjust (Obj : in out Ctrl); - procedure Finalize (Obj : in out Ctrl); - procedure Initialize (Obj : in out Ctrl); - -* :ada:`Initialize`, :ada:`Adjust` same definition as previously -* :ada:`Finalize` has the :ada:`No_Raise` aspect: it cannot raise exceptions -* :ada:`Relaxed_Finalization` - - * Performance on-par with C++'s destructor - * No automatic finalization of **heap-allocated** objects - -======== -Lab -======== - +.. include:: 260_controlled_types/01-introduction.rst +.. include:: 260_controlled_types/02-ada_finalization.rst +.. include:: 260_controlled_types/03-example.rst .. include:: labs/260_controlled_types.lab.rst - -========= -Summary -========= - ---------- -Summary ---------- - -* Controlled types allow access to object construction, assignment, destruction -* `Ada.Finalization` can be expensive to use - - - Other mechanisms may be more efficient - - * But require more rigor in usage +.. include:: 260_controlled_types/99-summary.rst diff --git a/courses/fundamentals_of_ada/260_controlled_types/01-introduction.rst b/courses/fundamentals_of_ada/260_controlled_types/01-introduction.rst new file mode 100644 index 000000000..43e8ce2cc --- /dev/null +++ b/courses/fundamentals_of_ada/260_controlled_types/01-introduction.rst @@ -0,0 +1,18 @@ +============== +Introduction +============== + +------------------------- +Constructor / Destructor +------------------------- + +* Possible to specify behavior of object initialization, finalization, and assignment + + - Based on type definition + - Type must derive from `Controlled` or `Limited_Controlled` in package `Ada.Finalization` + +* This derived type is called a *controlled type* + + - User may override any or all subprograms in `Ada.Finalization` + - Default implementation is a null body + diff --git a/courses/fundamentals_of_ada/260_controlled_types/02-ada_finalization.rst b/courses/fundamentals_of_ada/260_controlled_types/02-ada_finalization.rst new file mode 100644 index 000000000..671b94173 --- /dev/null +++ b/courses/fundamentals_of_ada/260_controlled_types/02-ada_finalization.rst @@ -0,0 +1,85 @@ +================== +Ada.Finalization +================== + +--------------- +Package Spec +--------------- + +.. code:: Ada + + package Ada.Finalization is + + type Controlled is abstract tagged private; + procedure Initialize (Object : in out Controlled) + is null; + procedure Adjust (Object : in out Controlled) + is null; + procedure Finalize (Object : in out Controlled) + is null; + + type Limited_Controlled is abstract tagged limited private; + procedure Initialize (Object : in out Limited_Controlled) + is null; + procedure Finalize (Object : in out Limited_Controlled) + is null; + + private + -- implementation defined + end Ada.Finalization; + +------- +Uses +------- + +* Prevent "resource leak" + + - Logic centralized in service rather than distributed across clients + +* Examples: heap reclamation, "mutex" unlocking +* User-defined assignment + +---------------- +Initialization +---------------- + +* Subprogram `Initialize` invoked after object created + + - Either by object declaration or allocator + - Only if no explicit initialization expression + +* Often default initialization expressions on record components are sufficient + + - No need for an explicit call to `Initialize` + +* Similar to C++ constructor + +---------------- +Finalization +---------------- + +* Subprogram `Finalize` invoked just before object is destroyed + + - Leaving the scope of a declared object + - Unchecked deallocation of an allocated object + +* Similar to C++ destructor + +------------ +Assignment +------------ + +* Subprogram `Adjust` invoked as part of an assignment operation +* Assignment statement `Target := Source;` is basically: + + - `Finalize (Target)` + - Copy Source to Target + - `Adjust (Target)` + - *Actual rules are more complicated, e.g. to allow cases where Target and Source are the same object* + +* Typical situations where objects are access values + + - `Finalize` does unchecked deallocation or decrements a reference count + - The copy step copies the access value + - `Adjust` either clones a "deep copy" of the referenced object or increments a reference count + diff --git a/courses/fundamentals_of_ada/260_controlled_types/03-example.rst b/courses/fundamentals_of_ada/260_controlled_types/03-example.rst new file mode 100644 index 000000000..aab34d4ba --- /dev/null +++ b/courses/fundamentals_of_ada/260_controlled_types/03-example.rst @@ -0,0 +1,123 @@ +========= +Example +========= + +---------------------------------- +Unbounded String Via Access Type +---------------------------------- + +* Type contains a pointer to a string type +* We want the provider to allocate and free memory "safely" + + - No sharing + - `Adjust` allocates referenced String + - `Finalize` frees the referenced String + - Assignment deallocates target string and assigns copy of source string to target string + +------------------------ +Unbounded String Usage +------------------------ + +.. code:: Ada + + with Unbounded_String_Pkg; use Unbounded_String_Pkg; + procedure Test is + U1 : Ustring_T; + begin + U1 := To_Ustring_T ("Hello"); + declare + U2 : Ustring_T; + begin + U2 := To_Ustring_T ("Goodbye"); + U1 := U2; -- Reclaims U1 memory + end; -- Reclaims U2 memory + end Test; -- Reclaims U1 memory + +----------------------------- +Unbounded String Definition +----------------------------- + +.. code:: Ada + + with Ada.Finalization; use Ada.Finalization; + package Unbounded_String_Pkg is + -- Implement unbounded strings + type Ustring_T is private; + function "=" (L, R : Ustring_T) return Boolean; + function To_Ustring_T (Item : String) return Ustring_T; + function To_String (Item : Ustring_T) return String; + function Length (Item : Ustring_T) return Natural; + function "&" (L, R : Ustring_T) return Ustring_T; + private + type String_Ref is access String; + type Ustring_T is new Controlled with record + Ref : String_Ref := new String (1 .. 0); + end record; + procedure Finalize (Object : in out Ustring_T); + procedure Adjust (Object : in out Ustring_T); + end Unbounded_String_Pkg; + +--------------------------------- +Unbounded String Implementation +--------------------------------- + +.. code:: Ada + + with Ada.Unchecked_Deallocation; + package body Unbounded_String_Pkg is + procedure Free_String is new Ada.Unchecked_Deallocation + (String, String_Ref); + + function "=" (L, R : Ustring_T) return Boolean is + (L.Ref.all = R.Ref.all); + + function To_Ustring_T (Item : String) return Ustring_T is + (Controlled with Ref => new String'(Item)); + + function To_String (Item : Ustring_T) return String is + (Item.Ref.all); + + function Length (Item : Ustring_T) return Natural is + (Item.Ref.all'Length); + + function "&" (L, R : Ustring_T) return Ustring_T is + (Controlled with Ref => new String'(L.Ref.all & R.Ref.all); + + procedure Finalize (Object : in out Ustring_T) is + begin + Free_String (Object.Ref); + end Finalize; + + procedure Adjust (Object : in out Ustring_T) is + begin + Object.Ref := new String'(Object.Ref.all); + end Adjust; + end Unbounded_String_Pkg; + +------------------ +Finalizable Aspect +------------------ + +* Uses the GNAT-specific :ada:`with Finalizable` aspect + +.. code:: Ada + + type Ctrl is record + Id : Natural := 0; + end record + with Finalizable => (Initialize => Initialize, + Adjust => Adjust, + Finalize => Finalize, + Relaxed_Finalization => True); + + procedure Adjust (Obj : in out Ctrl); + procedure Finalize (Obj : in out Ctrl); + procedure Initialize (Obj : in out Ctrl); + +* :ada:`Initialize`, :ada:`Adjust` same definition as previously +* :ada:`Finalize` has the :ada:`No_Raise` aspect: it cannot raise exceptions +* :ada:`Relaxed_Finalization` + + * Performance on-par with C++'s destructor + * No automatic finalization of **heap-allocated** objects + diff --git a/courses/fundamentals_of_ada/260_controlled_types/99-summary.rst b/courses/fundamentals_of_ada/260_controlled_types/99-summary.rst new file mode 100644 index 000000000..0827dbb51 --- /dev/null +++ b/courses/fundamentals_of_ada/260_controlled_types/99-summary.rst @@ -0,0 +1,14 @@ +========= +Summary +========= + +--------- +Summary +--------- + +* Controlled types allow access to object construction, assignment, destruction +* `Ada.Finalization` can be expensive to use + + - Other mechanisms may be more efficient + + * But require more rigor in usage diff --git a/courses/fundamentals_of_ada/270_introduction_to_contracts.rst b/courses/fundamentals_of_ada/270_introduction_to_contracts.rst index eb25aecd9..8e24ecefd 100644 --- a/courses/fundamentals_of_ada/270_introduction_to_contracts.rst +++ b/courses/fundamentals_of_ada/270_introduction_to_contracts.rst @@ -15,6 +15,9 @@ Ada Contracts .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/273_subprogram_contracts.rst b/courses/fundamentals_of_ada/273_subprogram_contracts.rst index b3ab6be06..4f6c99268 100644 --- a/courses/fundamentals_of_ada/273_subprogram_contracts.rst +++ b/courses/fundamentals_of_ada/273_subprogram_contracts.rst @@ -15,6 +15,9 @@ Subprogram Contracts .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/276_type_contracts.rst b/courses/fundamentals_of_ada/276_type_contracts.rst index 59caa9b67..fc416556a 100644 --- a/courses/fundamentals_of_ada/276_type_contracts.rst +++ b/courses/fundamentals_of_ada/276_type_contracts.rst @@ -15,6 +15,9 @@ Type Contracts .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` diff --git a/courses/fundamentals_of_ada/280_low_level_programming.rst b/courses/fundamentals_of_ada/280_low_level_programming.rst index 1a73ea9eb..f260c69b5 100644 --- a/courses/fundamentals_of_ada/280_low_level_programming.rst +++ b/courses/fundamentals_of_ada/280_low_level_programming.rst @@ -15,6 +15,9 @@ Low Level Programming .. role:: cpp(code) :language: C++ +.. role:: rust(code) + :language: Rust + .. container:: PRELUDE SYMBOLS .. |rightarrow| replace:: :math:`\rightarrow` @@ -33,783 +36,10 @@ Low Level Programming .. container:: PRELUDE END -============== -Introduction -============== - --------------- -Introduction --------------- - -* Sometimes you need to get your hands dirty -* Hardware Issues - - - Register or memory access - - Assembler code for speed or size issues - -* Interfacing with other software - - - Object sizes - - Endianness - - Data conversion - -===================== -Data Representation -===================== - -------------------------------------- -Data Representation Vs Requirements -------------------------------------- - -* Developer usually defines requirements on a type - - .. code:: Ada - - type My_Int is range 1 .. 10; - -* The compiler then generates a representation for this type that can accommodate requirements - - - In GNAT, can be consulted using ``-gnatR2`` switch - -.. code:: Ada - - -- with aspects - type Some_Integer_T is range 1 .. 10 - with Object_Size => 8, - Value_Size => 4, - Alignment => 1; - - -- with representation clauses - type Another_Integer_T is range 1 .. 10; - for Another_Integer_T'Object_Size use 8; - for Another_Integer_T'Value_Size use 4; - for Another_Integer_T'Alignment use 1; - -* These values can be explicitly set, the compiler will check their consistency -* They can be queried as attributes if needed - - .. code:: Ada - - X : Integer := My_Int'Alignment; - ---------------------- -Value_Size / Size ---------------------- - -* :ada:`Value_Size` (or :ada:`Size` in the Ada Reference Manual) is the minimal number of bits required to represent data - - - For example, :ada:`Boolean'Size = 1` - -* The compiler is allowed to use larger size to represent an actual object, but will check that the minimal size is enough - -.. code:: Ada - - -- with aspect - type Small_T is range 1 .. 4 - with Size => 3; - - -- with representation clause - type Another_Small_T is range 1 .. 4; - for Another_Small_T'Size use 3; - ------------------------------ -Object Size (GNAT-Specific) ------------------------------ - -* :ada:`Object_Size` represents the size of the object in memory -* It must be a multiple of :ada:`Alignment * Storage_Unit (8)`, and at least equal to :ada:`Size` - -.. code:: Ada - - -- with aspects - type Some_T is range 1 .. 4 - with Value_Size => 3, - Object_Size => 8; - - -- with representation clauses - type Another_T is range 1 .. 4; - for Another_T'Value_Size use 3; - for Another_T'Object_Size use 8; - -* Object size is the *default* size of an object, can be changed if specific representations are given - ------------ -Alignment ------------ - -* Number of bytes on which the type has to be aligned -* Some alignment may be more efficient than others in terms of speed (e.g. boundaries of words (4, 8)) -* Some alignment may be more efficient than others in terms of memory usage - -.. code:: Ada - - -- with aspects - type Aligned_T is range 1 .. 4 - with Size => 4, - Alignment => 8; - - -- with representation clauses - type Another_Aligned_T is range 1 .. 4; - for Another_Aligned_T'Size use 4; - for Another_Aligned_T'Alignment use 8; - --------------- -Record Types --------------- - -.. container:: columns - - .. container:: column - - * Ada doesn't force any particular memory layout - * Depending on optimization of constraints, layout can be optimized for speed, size, or not optimized - - .. code:: Ada - - type Enum is (E1, E2, E3); - type Rec is record - A : Integer; - B : Boolean; - C : Boolean; - D : Enum; - end record; - - .. container:: column - - .. image:: record_packing_examples.png - :width: 50% - -------------- -Pack Aspect -------------- - -* :ada:`Pack` aspect (or pragma) applies to composite types (record and array) -* Compiler optimizes data for size no matter performance impact -* Unpacked - - .. code:: Ada - - type Enum is (E1, E2, E3); - type Rec is record - A : Integer; - B : Boolean; - C : Boolean; - D : Enum; - end record; - type Ar is array (1 .. 1000) of Boolean; - -- Rec'Size is 56, Ar'Size is 8000 - -* Packed - - .. code:: Ada - - type Enum is (E1, E2, E3); - type Rec is record - A : Integer; - B : Boolean; - C : Boolean; - D : Enum; - end record with Pack; - type Ar is array (1 .. 1000) of Boolean; - pragma Pack (Ar); - -- Rec'Size is 36, Ar'Size is 1000 - -------------------------------- -Enum Representation Clauses -------------------------------- - -* Can specify representation for each value -* Representation must have increasing number - -.. code:: Ada - - type E is (A, B, C); - for E use (A => 2, B => 4, C => 8); - -* Can use :ada:`E'Enum_Rep (A) = 2` -* Can use :ada:`E'Enum_Val (2) = A` - -------------------------------- -Record Representation Clauses -------------------------------- - -.. container:: columns - - .. container:: column - - * Exact mapping between a record and its binary representation - * Optimization purposes, or hardware requirements - - - Driver mapped on the address space, communication protocol... - - * Fields represented as - - .. code:: Ada - - at range - .. - - - .. container:: column - - .. code:: Ada - - type Rec1 is record - A : Integer range 0 .. 4; - B : Boolean; - C : Integer; - D : Enum; - end record; - for Rec1 use record - A at 0 range 0 .. 2; - B at 0 range 3 .. 3; - C at 0 range 4 .. 35; - -- unused space here - D at 5 range 0 .. 2; - end record; - ------------------- -Unchecked Unions ------------------- - -* Allows replicating C's :c:`union` with **discriminated** records -* Discriminant is **not stored** -* No discriminant check -* Object must be **mutable** - -.. code:: Ada - - type R (Is_Float : Boolean := False) is record - case Is_Float is - when True => - F : Float; - when False => - I : Integer; - end case; - end record - with Unchecked_Union; - - O : R := (Is_Float => False, I => 1); - F : Float := R.F; -- no check! - ------------------------------- -Array Representation Clauses ------------------------------- - -* :ada:`Component_Size` for array's **component's** size - -.. code:: Ada - - -- with aspect - type Array_T is array (1 .. 1000) of Boolean - with Component_Size => 2; - - -- with representation clause - type Another_Array_T is array (1 .. 1000) of Boolean; - for Another_Array_T'Component_Size use 2; - --------------------------- -Endianness Specification --------------------------- - -* :ada:`Bit_Order` for a type's endianness -* :ada:`Scalar_Storage_Order` for composite types - - - Endianess of components' ordering - - GNAT-specific - - Must be consistent with :ada:`Bit_Order` - -* Compiler will peform needed bitwise transformations when performing operations - -.. code:: Ada - - -- with aspect - type Array_T is array (1 .. 1000) of Boolean with - Scalar_Storage_Order => System.Low_Order_First; - - -- with representation clauses - type Record_T is record - A : Integer; - B : Boolean; - end record; - for Record_T use record - A at 0 range 0 .. 31; - B at 0 range 32 .. 33; - end record; - for Record_T'Bit_Order use System.High_Order_First; - for Record_T'Scalar_Storage_Order use System.High_Order_First; - --------------------------- -Change of Representation --------------------------- - -* Explicit new type can be used to set representation -* Very useful to unpack data from file/hardware to speed up references - -.. code:: Ada - - type Rec_T is record - Field1 : Unsigned_8; - Field2 : Unsigned_16; - Field3 : Unsigned_8; - end record; - type Packed_Rec_T is new Rec_T; - for Packed_Rec_T use record - Field1 at 0 range 0 .. 7; - Field2 at 0 range 8 .. 23; - Field3 at 0 range 24 .. 31; - end record; - R : Rec_T; - P : Packed_Rec_T; - ... - R := Rec_T (P); - P := Packed_Rec_T (R); - -.. container:: speakernote - - Size of R is probably 48 (for 16-bit alignment) or 96 (for 32-bit alignment) - Size of P will always be 32 - -============================== -Address Clauses and Overlays -============================== - ---------- -Address ---------- - -* Ada distinguishes the notions of - - - A reference to an object - - An abstract notion of address (:ada:`System.Address`) - - The integer representation of an address - -* Safety is preserved by letting the developer manipulate the right level of abstraction -* Conversion between pointers, integers and addresses are possible -* The address of an object can be specified through the :ada:`Address` aspect - ------------------ -Address Clauses ------------------ - -* Ada allows specifying the address of an entity - - .. code:: Ada - - Use_Aspect : Unsigned_32 with - Address => 16#1234_ABCD#; - - Use_Rep_Clause : Unsigned_32; - for Use_Rep_Clause'Address use 16#5678_1234#; - -* Very useful to declare I/O registers - - - For that purpose, the object should be declared volatile: - - .. code:: Ada - - Use_Aspect : Unsigned_32 with - Volatile, - Address => 16#1234_ABCD#; - - Use_Rep_And_Pragma : Unsigned_32; - for Use_Rep_And_Pragma'Address use 16#5678_1234#; - pragma Volatile (Use_Rep_And_Pragma); - -* Useful to read a value anywhere - - .. code:: Ada - - function Get_Byte (Addr : Address) return Unsigned_8 is - V : Unsigned_8 with Address => Addr, Volatile; - begin - return V; - end; - - - In particular the address doesn't need to be constant - - But must match alignment - ----------------- -Address Values ----------------- - -* The type :ada:`Address` is declared in :ada:`System` - - - But this is a :ada:`private` type - - You cannot use a number - -* Ada standard way to set constant addresses: - - - Use :ada:`System.Storage_Elements` which allows arithmetic on address - - .. code:: Ada - - V : Unsigned_32 with - Address => - System.Storage_Elements.To_Address (16#120#); - -* GNAT specific attribute :ada:`'To_Address` - - - Handy but not portable - - .. code:: Ada - - V : Unsigned_32 with - Address => System'To_Address (16#120#); - ----------- -Volatile ----------- - -* The :ada:`Volatile` property can be set using an aspect or a pragma -* Ada also allows volatile types as well as objects - - .. code:: Ada - - type Volatile_U32 is mod 2**32 with Volatile; - type Volatile_U16 is mod 2**16; - pragma Volatile (Volatile_U16); - -* The exact sequence of reads and writes from the source code must appear in the generated code - - - No optimization of reads and writes - -* Volatile types are passed by-reference - ---------------------- -Ada Address Example ---------------------- - -.. code:: Ada - - type Bit_Array_T is array (Integer range <>) of Boolean - with Component_Size => 1; - - -- objects can be referenced elsewhere - Object : aliased Integer with Volatile; - Object2 : aliased Integer with Volatile; - - Object_A : System.Address := Object'Address; - Object_I : Integer_Address := To_Integer (Object_A); - - -- This overlays Bit_Array_Object onto Object in memory - Bit_Array_Object : aliased Bit_Array_T (1 .. Object'Size) - with Address => Object_A; - - Object2_Alias : aliased Integer - -- Trust me, I know what I'm doing, this is Object2 - with Address => To_Address (Object_I - 4); - --------------------- -Aliasing Detection --------------------- - -* :dfn:`Aliasing`: multiple objects are accessing the same address - - - Types can be different - - Two pointers pointing to the same address - - Two references onto the same address - - Two objects at the same address - -* :ada:`Var1'Has_Same_Storage (Var2)` checks if two objects occupy exactly the same space -* :ada:`Var'Overlaps_Storage (Var2)` checks if two object are partially or fully overlapping - ----------------------- -Unchecked Conversion ----------------------- - -* :ada:`Unchecked_Conversion` allows an unchecked *bitwise* conversion of data between two types -* Needs to be explicitly instantiated - - .. code:: Ada - - type Bitfield is array (1 .. Integer'Size) of Boolean; - function To_Bitfield is new - Ada.Unchecked_Conversion (Integer, Bitfield); - V : Integer; - V2 : Bitfield := To_Bitfield (V); - -* Avoid conversion if the sizes don't match - - - Not defined by the standard - - Many compilers will warn if the type sizes do not match - -======== -Tricks -======== - --------------------- -Package Interfaces --------------------- - -* Package :ada:`Interfaces` provide Integer and unsigned types for many sizes - - - :ada:`Integer_8`, :ada:`Integer_16`, :ada:`Integer_32`, :ada:`Integer_64` - - :ada:`Unsigned_8`, :ada:`Unsigned_16`, :ada:`Unsigned_32`, :ada:`Unsigned_64` - -* With shift/rotation functions for unsigned types - ------------------------------- -Fat/Thin Pointers for Arrays ------------------------------- - -* Unconstrained array access is a fat pointer - - .. code:: Ada - - type String_Acc is access String; - Msg : String_Acc; - -- array bounds stored outside array pointer - -* Use a size representation clause for a thin pointer - - .. code:: Ada - - type String_Acc is access String; - for String_Acc'Size use 32; - -- array bounds stored as part of array pointer - -------------- -Flat Arrays -------------- - -* A constrained array access is a thin pointer - - - No need to store bounds - - .. code:: Ada - - type Line_Acc is access String (1 .. 80); - -* You can use big flat array to index memory - - - See :ada:`GNAT.Table` - - Not portable - - .. code:: Ada - - type Char_array is array (natural) of Character; - type C_String_Acc is access Char_Array; - -======== -Lab -======== - +.. include:: 280_low_level_programming/01-introduction.rst +.. include:: 280_low_level_programming/02-data_representation.rst +.. include:: 280_low_level_programming/03-address_clauses_and_overlays.rst +.. include:: 280_low_level_programming/04-tricks.rst .. include:: labs/280_low_level_programming.lab.rst - -========= -Summary -========= - ---------- -Summary ---------- - -* Like C, Ada allows access to assembly-level programming -* Unlike C, Ada imposes some more restrictions to maintain some level of safety -* Ada also supplies language constructs and libraries to make low level programming easier - -================================== -Supplementary Resource: Inline ASM -================================== - ------------------------ -Calling Assembly Code ------------------------ - -* Calling assembly code is a vendor-specific extension -* GNAT allows passing assembly with :ada:`System.Machine_Code.ASM` - - - Handled by the linker directly - -* The developer is responsible for mapping variables on temporaries or registers -* See documentation - - - GNAT RM 13.1 Machine Code Insertion - - GCC UG 6.39 Assembler Instructions with C Expression Operands - ------------------- -Simple Statement ------------------- - -* Instruction without inputs/outputs - - .. code:: Ada - - Asm ("halt", Volatile => True); - - - You may specify :ada:`Volatile` to avoid compiler optimizations - - In general, keep it False unless it created issues - -* You can group several instructions - - .. code:: Ada - - Asm ("nop" & ASCII.LF & ASCII.HT - & "nop", Volatile => True); - Asm ("nop; nop", Volatile => True); - -* The compiler doesn't check the assembly, only the assembler will - - - Error message might be difficult to read - ----------- -Operands ----------- - -* It is often useful to have inputs or outputs... - - - :ada:`Asm_Input` and :ada:`Asm_Output` attributes on types - -.. image:: annotated_assembly_statement.png - :width: 85% - ------------------------------------------ -Mapping Inputs / Outputs on Temporaries ------------------------------------------ - -.. code:: Ada - - Asm (