00 - Intro to Rust
We will use the Rust programming language for the labs.
Resources
- The Rust Programming Language, Chapters 1, 2, 3, 4 and 5
- Tour of Rust step by step tutorial
- Let's Get Rusty - The Rust Lang Book
This lab is rather long, but it tries to be a really quick intro to Rust. We suggest going directly to the Exercises, solve one by one, and read the required documentation as you go.
Standard library
The standard library is divided into three levels:
Level | Description | Needs |
---|---|---|
core | Provides the required language elements that Rust needs for compiling, like the Display and Debug traits. Data can only be global items (stored in .data) or on the stack. | Hardware |
alloc | Provides everything from the core level plus heap allocated data structures like, Box and Vec . The developer has to provide a memory allocator, like embedded_alloc. | Memory Allocator |
std | Provides everything from the alloc level plus a lot of features that depend on the platform, including threads and I/O. This is the default level for Windows, Linux, macOS and similar OSes applications. | Operating System |
This course will mostly use the core
level of the standard library, as the software has to run on a Raspberry Pi Pico.
By default, Rust has a set of elements defined in the standard library that are imported into the program of each application. This set is called the prelude, and you can look it up in the standard library documentation.
If a type you want to use is not in the prelude, you must bring that type into scope explicitly with a use
statement. Using the std::io
module gives you a number of useful features, including the ability to accept
user input.
use std::io;
The main
function
The main
function is the entry point of our program.
fn main() {
println!("Hello, world!");
}
We use println!
macro to print messages on the screen.
To print more complex variables, you can use {:?}
, which ensures that any type that implements the Debug trait can be printed.
To insert a placeholder in the println!
macro, use a pair of braces {}
. We provide the variable name or
expression to replace the provided placeholder outside the string.
fn main() {
let name = "Mary";
let age = 26;
println!("Hello, {}. You are {} years old", name, age);
// if the replacements are only variable, one can use the inline version
println!("Hello, {name}. You are {age} years old");
}
Variables and mutability
We use the let
keyword to create a variable.
let a = 5;
By default, in Rust, variables are immutable , meaning once a value is tied to a name, you cannot change that value.
Example:
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
In this case, we will get a compilation error because we are trying to modify the value of x
from 5
to 6
, but
x
is immutable, so we cannot make this modification.
Although variables are immutable by default, you can make them mutable by adding mut
in front of the
variable name. Adding mut
also conveys intent to future readers of the code by indicating that other parts
of the code will modify the value of this variable.
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Now the value of x
can become 6
.
Constants
Like immutable variables, constants are values that are tied to a name and have a value known at compile time.
You are not allowed to use mut
with constants. Constants are not only immutable by default,
they are always immutable. You declare constants using the const
keyword instead of the let
keyword.
Constants's data type has to be specified at declaration.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
For a better understanding, please read chapter 3 of the documentation.
Data Types
Scalar types
A scalar type represents a single value. Rust has four main scalar types: integers, floating point numbers, booleans, and characters.
Integer → Each variant can be signed or unsigned and has an explicit size.
let x: i8 = -2;
let y: u16 = 25;
Length | Signed | Unsigned | Java Equivalent | C Equivalent1 |
---|---|---|---|---|
8-bit | i8 | u8 | byte / Byte 2 | char / unsigned char |
16-bit | i16 | u16 | short / Short 2 | short / unsigned short |
32-bit | i32 | u32 | int / Integer 2 | int / unsigned int |
64-bit | i64 | u64 | long / Long 2 | long long / unsigned long long |
128-bit | i128 | u128 | N/A | N/A |
arch | isize | usize | N/A | int / unsigned int |
Floating Point → Rust's floating point types are f32
and f64
, which are 32-bit and 64-bit in size, respectively. The default type is f64
because on modern CPUs it is about the same speed as f32
but is capable of more precision. All floating point types are signed.
Length | Floating point | Java Equivalent | C Equivalent |
---|---|---|---|
32-bit | f32 | float | float |
64-bit | f64 | double | double |
128-bit | f128 | N/A | N/A |
fn main() {
let x = 2.0; // f64
let y1: f32 = 3.0; // f32
let y2 = 3.0f32; // f32
}
Boolean → Booleans are one byte in size. Boolean type in Rust is specified using bool.
let t = true;
let f: bool = false; // with explicit type annotation
Character → The Rust char type is the most primitive alphabetic type in the language.
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
Structures
Structs are a data type construct that contain other data types in the form of fields. Rust structures are similar to C structs and Java classes.
To define a structure, we enter the struct
keyword and name the entire structure. Then, within curly brackets, we define the names and types of the data, which we call fields.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
To use a structure after having defined it, we create an instance of this structure by specifying concrete values for each of the fields. We create a stack allocated instance by specifying the structure name , then add curly braces containing key: value
pairs , where the keys are the field names and the values are the data we want to store in those fields.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
To access a certain member of the structure we use this syntax:
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com")
}
Note that the entire instance must be editable ; Rust doesn't allow us to mark only certain fields as mutable!
We can construct a new instance of the structure as the last expression in the function body to implicitly return this new instance.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Structure Implementation
Structures in Rust are similar to classes in OOP. Besides the operations mentioned above, structures can also be implemented and have methods specific to a structure. The methods are defined in the structure's implementation.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
impl User {
// static method (no self parameter)
// called with User::new()
fn new() -> User {
// ...
}
// method
// called user.is_active()
fn is_active (&self) -> bool {
return self.active;
}
}
Printing Structures
If we try to print an instance of User
using the println!
macro as we have seen early, it will not work.
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
println!("User is: {}", user1);
}
We will get the following error message:
error[E0277]: `User` doesn't implement `std::fmt::Display`
In order to print a structure, we need to use {:?}
instead of {}
, and implement Debug
trait for the structure with #[derive(Debug)]
.
We use Debug
trait to print structures, arrays, enums or any other type that doesn't implement Display
.
#[derive(Debug)]
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
println!("User is: {:?}", user1);
}
Output:
User is: User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1 }
To format the Debug
nicely use {:#?}
.
Tuple structures
Tuples are the same construct as structures, just that instead of using names for their field, they use numbers (indexes).
struct Color(i32, i32, i32);
struct Device(String, u8);
fn main() {
let black = Color(0, 0, 0);
let device = Device(String::from("Raspberry Pi Pico"), 2);
println!("The device type is {} and the version is {}", device.0, device.1);
}
Tuples can be named (as the one shown above) or can be anonymous. The following example shows how functions use anonymous tuples to return multiple values.
fn get_item_and_index(value: &str) -> (String, usize) {
// usually search the value here
(String::from("the name"), 0)
}
let value = get_item_and_index("...");
// use value.0 and value.1
For a better understanding, please read chapter 5 of the documentation.
Enums
Enumerations, also referred as enums
, allow you to define a type by enumerating its possible variants.
How to define an enum
:
enum IpAddrKind {
V4,
V6,
}
Option
enum
Option
is another enum
defined by the standard library. The Option
type encodes the very common scenario in which a value can be something or nothing.
Rust doesn't have the null functionality that many other languages have. Null is a value that means there is no value here. In languages with null, variables can always be in one of two states: null or non-null.
As such, Rust does not have null values, but it does have an enumeration that can encode the concept of a value being present or absent. This enumeration is Option<T>
, and it is defined by the standard library as follows:
enum Option<T> {
None,
Some(T),
}
For now, all you need to know is that <T>
means that the Some variant of the Option enumeration can contain data of any type.
fn integer_division (a:isize, b: isize) -> Option<isize> {
if b == 0 {
None
} else {
Some(a / b)
}
}
When we have a Some
value, we know that a value is present and that the value is contained in Some
. When we have a None
value, it kind of means the same thing as null: we don't have a valid value.
You must convert an Option<T>
to a T
before you can perform T
operations with it.
Match
Rust has an extremely powerful control flow construct called match
that allows you to compare a value against a series of patterns and then run code based on which pattern matches. Patterns can consist of literal values, variable names, wildcards, and many other things.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
When the match expression runs, it compares the resulting value to the model for each arm, in order. If a pattern matches the value, the code associated with that pattern is executed. If this pattern does not match the value, execution continues to the next arm.
The code associated with each arm is an expression , and the resulting value of the expression in the corresponding arm is the returned value for the entire matching expression.
In the previous section, we wanted to extract the internal T
value of the Some case when using Option<T>
; we can also handle Option<T>
using match , like we did with the Coin
enumeration! Instead of comparing parts, we will compare variants of Option<T>
, but the way the match
expression works remains the same.
fn main () {
let x = 120;
let y = 7;
match integer_division (x, y) {
Some(d) => println! ("{}:{} = {}", x, y, d),
None => println! ("division by 0")
};
}
Result
enum
Result
is an enum
whose purpose is to encode error management information:
- The variant
Ok
indicates that the operation was successful and within theOk
the intended result is wrapped - The variant
Err
indicates that the operation had an error somewhere and within theErr
a struct/enum with more information about the error that happened
The definition given by the standard library is:
enum Result<T,E> {
Ok(T),
Err(E),
}
where T
and E
are generic types and mean you may have anything within an Ok
or an Err
Example:
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => {
// use the file variable here
}
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
The ?
operator
You may place a ?
after a call that returns a result value, and it will immediatelly propagate the error to the caller, otherwise continue as if the expression is the value inside the Ok
variant
Example:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
For a better understanding, please read chapter 6 of the documentation.
Tuple → A tuple is a structure used for grouping a number of values with a variety of types into a single compound type. Tuples have a fixed length : once declared, their size cannot increase or decrease.
let tup: (i32, f64, u8) = (500, 6.4, 1);
Array → Unlike a tuple, each element in an array must have the same type. Unlike arrays in some other languages, Rust arrays have a fixed length.
let a = [1, 2, 3, 4, 5];
For a better understanding, please read chapter 3 of the documentation.
Functions
We define a function in Rust by entering fn
keyword followed by a function name and a set of parentheses. Curly braces tell the compiler where the function body begins and ends.
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Parameters
We can define functions with parameters, which are special variables that are part of a function's signature. When a function has parameters, you can provide it with concrete values for those parameters, also called arguments.
fn main() {
// the `another_function` function call has one single argument, the value 5.
another_function(5);
}
// the `another_function`function has one single parameter `x` of type `i32`
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
In function signatures you must declare the type of each parameter!
Functions with return values
Functions can return values to the code that calls them. We don't name the return values, but we must declare their type after an arrow (->
). In Rust, the function's return value is synonymous with the value of the final expression in a function's body block. You can return earlier from a function by using the return
keyword and specifying a value, but most functions implicitly return the last expression.
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");// "The value of x is: 5"
}
For a better understanding, please read chapter 3 of the documentation.
Control flow
if-else
All if
expressions start with the if
keyword , followed by a condition. Optionally, we can also include an else
expression.
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
You can use multiple conditions by combining if
and else
in an else if
expression:
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Because if
is an expression, we can use it on the right side of a let
statement to assign the result to a variable.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");//"The value of the number is 5"
}
loop
The loop
keyword tells Rust to run a block of code over and over forever or until you explicitly tell it to stop.
fn main() {
loop {
println!("again!");
}
}
One use of a loop
is to retry an operation that you know might fail, such as checking if a thread has finished its work. You may also need to pass the result of this operation out of the loop to the rest of your code. To do this, you can add the value you want to return after the break
expression you use to stop the loop; this value will be returned out of the loop so you can use it:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
while
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
for
In Rust, the for structure is used to iterate over a list of elements (vec). With each iteration, a reference to an element in the list is returned.
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
For a better understanding, please read chapter 3 of the documentation.
Complex Data Types
Vec
The data type that Rust standard library provides for storing a list of data is Vec
. This is similar ot the C++ vector
and Java ArrayList
.
The data type of a vector is Vec<T>
, where T
can be any data type.
To create a new vector, Rust provides the vec!
macro. A longer vay of writing is Vec::new()
.
let v = vec![];
// or
let v = Vec::new();
The actual type of T
should be inferred by the compiler.
Sometimes the compiler is not able to infer the data type and we must provide it.
let v: Vec<String> = vec![];
// or
let v = Vec::<String>::new();
The Vec
type provides several functions to insert, access and delete elements.
Method | Description | Returned Data Type |
---|---|---|
len() | Number of elements in the vector | usize |
push(t: T) | Add an element of type T to the end of the vector | () |
get(index: usize) | Get a reference to one of the elements in the vector | Option<&T> |
get_mut(index:usize) | Get a mutable reference to one of the elements in the vector | Option<&mut T> |
remove(index:usize) | Remove the element at an index. | T |
The remove
function will panic if the index
is out of bounds.
The best way to iterate of all the elements in a Vec
is using a for
.
for element in v {
// use element of type &T
}
String
Rust has only one type of string in the core language, which is the string slice str
which is usually seen in its borrowed form &str
.
The String
type , which is provided by the Rust standard library rather than encoded in the main language, is a scalable, mutable, and owned UTF-8 encoded string type .
Creating a new String
let mut s = String::new();
This line creates a new empty string called s
, which we can then load data into.
We can use the String::from
function or the to_string
function to create a string from a string literal:
let s = String::from("initial contents");
let data = "initial contents";
let s = data.to_string();
// the method also works on a literal directly:
let s = "initial contents".to_string();
Adding to a string
We can expand a string using the push_str
method to add a string slice.
let mut s = String::from("foo");
s.push_str("bar");
The push method takes a single character as a parameter and adds it to the string.
let mut s = String::from("lo");
s.push('l');
Iteration Methods on Strings
The best way to operate on pieces of strings is to be explicit about whether you want characters or bytes. For individual Unicode scalar values, use the chars
method .
for c in "Зд".chars() {
println!("{}", c);
}
Run the program
In order to run the program we may be anywhere in the crate's folder and execute the command:
cargo run
Exercises
If you don't have Rust installed, you can use Rust Playground to solve the topics.
Before tackling the exercises, take a look and cover chapters 1, 2 and 3 of Tour of Rust tutorials.
- Write a function that takes your name as a parameter and greets you to
stdout
(prints on the screen). What type should the parameter have and why? (1p). - Write a function that takes an unsigned integer
N
as a parameter and prints out the firstN
odd numbers (1p). - Write a function that returns the first even number from an array slice. Make sure to handle the case when there is no even number. (1p) Hint: a slice is a part of an array,
&a[first..end]
. Take a look atfor
andOption
. Keep in mind that for gives a reference to each element in the list. - Write a function that looks through a vector of strings and returns the first element that has more than 4 characters (1p) Hint: Take a look at
for
,Option
, and thefrom(), len() and to_string()
functions. - Define a vector of transactions that can either be in Ron, Dollars, Euros, Pounds and Bitcoin. Create a function that calculates the total value in Ron that the vector has
(assume Ron is 1, Dollar is 4.5, Euro is 5, Pound is 6 and Bitcoin is 100000) (2p) Hint: take a look at
enum
andstructures
. - Write a function that transforms a string slice
&str
into an unsigned integer, returning either the value, or an error code. Create an error type that handles the cases where string is empty, string has invalid character (and at which position) and when the number given is negative. (2p) Hint: Take a look atOption
andResult
- Define a structure
Complex
with floats. (2p) a. implement an associated static functionnew
for this structure. b. Implement 2 possible operations for it (which includes the absolute value and multiplication). c. Implement a display method that prints the number.
Ownership
Ownership is a set of rules that govern how a Rust program manages memory. All programs must manage the way they use the memory of a computer while running.
Some languages have garbage collection that regularly searches for unused memory during program execution, in other languages, the programmer must explicitly allocate and release memory.
Rust uses a third approach: the memory is managed via a property system with a set of rules that the compiler verifies. If one of the rules is violated, the program will not compile. None of the property characteristics will slow down your program during its execution.
Ownership rules
- Each value in Rust has an owner
- A value cannot have more than one owner at a time
- When the values are out of reach, they are dropped
Scope
A scope is the code area of a program in which an element is valid.
Here's an example to understand the concept:
{
// Here s is invalid
let s = "hello"; // s is valid past this point
} // after this the value s will be dropped
Ownership in functions
The mechanisms for transmitting a value to a function are similar to those of assigning a value to a variable. Switching a variable to a function will move or copy, just as the assignment does.
Example (read the comments):
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // a copy to x is passed to the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
If we were trying to use s after the call to take_ownership
, Rust would return a compilation error. These static checks protect us from errors.
Return values and scope
The return values can also transfer the ownership.
The possession of a variable follows the same pattern each time: the assignment of one value to another variable moves it. When a variable that includes data on the heap comes out of the scope, the value will be cleaned by drop unless the data ownership has been moved to another variable.
Example (read the comments):
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
References and borrowing
A reference is like a pointer in the sense that it is an address that we can track to access the data stored at that address; these data belong to another variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the lifetime of this reference.
The symbol &
is used to mark a reference, either before the name of a variable, or, for the case of a parameter of a function, before the type of the parameter. These sprinkles represent references and allow you to refer to a value without your own ownership.
let x: u16 = 10;
let y = &x;
Example of a function which takes a reference to an object as a parameter instead of taking possession of this value:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, it is not dropped.
The syntax s1
allows us to create a reference that refers to the value of s1 but does not. Since the reference does not have the value to which it points, the value of s1 will not be deleted when the reference ceases to be used.
Similarly, the signature of the function uses & to indicate that the type of parameter s is a reference.
We call borrowing the action of creating a reference. As in real life, you can borrow something from someone. When you don't need the borrowed thing anymore, you have to return it. You don't own it.
Just as the variables are immutable by default, so are the references. We are not allowed to change the value pointed to by a reference
Mutable references
If we want to change the value of a reference we have to say this explicitly to the compiler using the keyword mut
Mutable references have a great restriction: if you have a mutable reference to a value, you cannot have other references to that value.
Nor can we have a reference mutable while we have one immutable the same value.
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Rules for references:
- At any time, you can have only a mutable reference or any number of immutable references but not both.
- References must always be valid.
Copy trait
Let's take a similar code to the one presented before:
let mut x:i32 = 0;
let mut y = x;
y = 5;
println!("{x}"); // Prints 0
This time, the compiler seems to not have moved the variable x
into y
. Why? Because i32 implements Copy
. This is a trait used for types that are inexpensive to duplicate bit by bit, and which also do not allow 2 mutable references to the same location in memory.
Type | Implements Copy | Reason |
---|---|---|
i32 | Yes | |
f64 | Yes | |
bool | Yes | |
String | No | It holds a pointer to its internal buffer. The buffer had to be duplicated when copying, action that a byte by byte copy is not able to do. |
Vec<_> | No | It holds a pointer to its internal buffer. The buffer had to be duplicated when copying, action that a byte by byte copy is not able to do. |
&str | Yes | |
&mut str | No | Copying would create another mutable reference to the same value. |
You may implement the Copy
trait to your structs and enums by using #[derive(Clone, Copy)]
You must implement Clone
trait in order to derive Copy
. Also, all of the fields must have types that implement Copy
.
Bonus at home
- Rewrite the function at 2., but this time implement it using the Sieve of Erathostenes.
- Define a struct called MiniTuring, with a buffer of 256 booleans and a cursor.
- Write an associated (static) function called new that creates an instance of the structure.
- Write a method called display that prints the tape with 1's and 0's instead of trues and falses, without newlines or spaces in between
- Read the keyboard until "h" is received. "l" will move the cursor to the left with wrap around, "r" will move the cursor to the right with wrap around, "1" will set the element at the cursor to true, "0" will set the element at the cursor to false, "p" prints the value at cursor, "h" displays the tape
- Create a basic expression parser for integer numbers, which supports +,-,*,/. Assume unary - will not happen (no expressions like 5*-3, -2+7)
- Define an enum called Expression with the appropriate variants(hint: use
Box
) - Create a function that returns an Expression based on a given string. Respect operator precedence rules
- Creates a function that takes an Expression and evaluates it to an i32
- Read an expression from stdin and print out the result
Footnotes
-
The data types used here are considered for a 32 bit system, for other architectures the equivalent data types might differ (
short
is at least 2 bytes long). ↩ -
Starting with Java 8, the
Number
classes have some helper methods, likecompareUnsigned
andtoUnsigned...
that allow the usage and manipulation of unsigned numbers. ↩ ↩2 ↩3 ↩4