mirror of
https://github.com/LukasKalbertodt/programmieren-in-rust.git
synced 2025-06-29 00:07:31 +02:00
Rename sheet folders to assure proper sorting
This commit is contained in:
10
aufgaben/sheet04/README.md
Executable file
10
aufgaben/sheet04/README.md
Executable file
@ -0,0 +1,10 @@
|
||||
Blatt 4
|
||||
=======
|
||||
|
||||
Wie immer soll dieses Blatt in einem eigenen Branch, `sheet4`, bearbeitet
|
||||
werden.
|
||||
|
||||
Eine weitere generelle Aufgabe: Euer erster Commit soll *nur* die Dateien
|
||||
beinhalten, die euch schon (in diesem Repository) gegeben werden. So fällt es
|
||||
den Tutoren und mir deutlich leichter, den vorgegebenen Teil und eure eigenen
|
||||
Änderungen zu unterscheiden!
|
57
aufgaben/sheet04/sol1/good.rs
Executable file
57
aufgaben/sheet04/sol1/good.rs
Executable file
@ -0,0 +1,57 @@
|
||||
/// Prints all happy primes between 1 and 20.
|
||||
fn main() {
|
||||
for i in 1..21 {
|
||||
if is_happy_prime(i) {
|
||||
println!("{} is a happy prime!", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether the given number is a happy number AND a prime number.
|
||||
fn is_happy_prime(n: u64) -> bool {
|
||||
is_happy(n) && is_prime(n)
|
||||
}
|
||||
|
||||
/// Determines whether the given, positive number is a [happy number][1].
|
||||
///
|
||||
/// [1]: https://en.wikipedia.org/wiki/Happy_number
|
||||
fn is_happy(mut number: u64) -> bool {
|
||||
// Either we end as "1" or in a cycle
|
||||
while number > 1 {
|
||||
number = {
|
||||
// Here we compute the sum of squares of all digits. The trick is that
|
||||
// the last digit is `number % 10` and that we can remove the last
|
||||
// digit by dividing number by 10.
|
||||
let mut sum = 0;
|
||||
while number > 0 {
|
||||
let digit = number % 10;
|
||||
sum += digit * digit;
|
||||
number /= 10;
|
||||
}
|
||||
|
||||
sum
|
||||
};
|
||||
|
||||
// We ended up in a cycle -> not happy
|
||||
if number == 4 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Determines whether the given, positive, non-zero number is a prime number.
|
||||
fn is_prime(n: u64) -> bool {
|
||||
if n == 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
for divisor in 2..n {
|
||||
if n % divisor == 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
450
aufgaben/sheet04/sol2/calculator.rs
Executable file
450
aufgaben/sheet04/sol2/calculator.rs
Executable file
@ -0,0 +1,450 @@
|
||||
//! A simple command-line calculator
|
||||
//!
|
||||
|
||||
/// The integer type we use internally to hold the numbers.
|
||||
type Integer = i64;
|
||||
|
||||
/// Represents an operation our calculator can carry out.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Op {
|
||||
Add,
|
||||
Subtract,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
/// The operation is applied to the given `lhs` and `rhs`. If an overflow
|
||||
/// occured during the operation, None is returned.
|
||||
fn apply(&self, lhs: Integer, rhs: Integer) -> Option<Integer> {
|
||||
match *self {
|
||||
Op::Add => lhs.checked_add(rhs),
|
||||
Op::Subtract => lhs.checked_sub(rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a (part of a) calculation.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Expr {
|
||||
/// Just a literal number (no calculation needed)
|
||||
Literal(Integer),
|
||||
|
||||
/// An operation with two operands
|
||||
Op {
|
||||
/// Kind of operation
|
||||
operator: Op,
|
||||
|
||||
/// The Operands.
|
||||
///
|
||||
/// The task said the operands should be saved as `Vec<Expr>`
|
||||
/// and it would be fine to hand in such a solution, but a vector
|
||||
/// is actually the wrong type here. We know that we always have
|
||||
/// exactly two operands, so a type that can hold any number of items
|
||||
/// is not correct.
|
||||
///
|
||||
/// The correct type is a pair or an array with two elements. But this
|
||||
/// will lead to a problem: fields are saved inside of the type and
|
||||
/// therefore it's not possible to have a recursive definition. The
|
||||
/// only way to do it, is to introduce one layer of indirection. A
|
||||
/// vector is such a layer, because it saves its contents on the heap.
|
||||
///
|
||||
/// A better choice is `Box<T>`. As a vector, it stores its content
|
||||
/// on the heap and also owns the content. But unlike the vector, a box
|
||||
/// always saves one element only.
|
||||
operands: Box<[Expr; 2]>,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stuff that can go wrong during parsing. Of course, it's desirable to have
|
||||
/// even more information about an error, but this is not in the scope of this
|
||||
/// task. Apart from that, good error reporting is hard. That's why so many
|
||||
/// compiler (especially a few years ago) suck at it.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ParseError {
|
||||
/// Empty input
|
||||
Empty,
|
||||
|
||||
/// A token that was not expected at that position in the input
|
||||
UnexpectedToken(Token),
|
||||
|
||||
/// More tokens were expected
|
||||
UnexpectedEof,
|
||||
|
||||
/// There are unmatched parenthesis
|
||||
UnmatchedParens,
|
||||
|
||||
/// More tokens in the input found, although none were expected
|
||||
UnexpectedAdditionalTokens,
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
/// This function parses a list of tokens and returns the corresponding
|
||||
/// expression tree, if the parse was successful. The grammar for our
|
||||
/// expressions is:
|
||||
///
|
||||
/// ```
|
||||
/// expr := ⟨operand⟩ [⟨op⟩ ⟨operand⟩]
|
||||
/// operand := ⟨num⟩ | "(" ⟨expr⟩ ")"
|
||||
/// op := "+" | "-"
|
||||
/// num := "0" | "1" | ... | "9"
|
||||
/// ```
|
||||
///
|
||||
/// Parsing is difficult. Parsing mathematical infix notation like this can
|
||||
/// be done with the Shunting-yard algorithm [1]. But more general parsing
|
||||
/// algorithms to parse determinstic context-free grammars can be used,
|
||||
/// too. Recursive descent parsers are probably rather similar to the way
|
||||
/// humans think about a grammar.
|
||||
///
|
||||
/// This implementation does not implement a concrete algorithm (as far as
|
||||
/// I know).
|
||||
///
|
||||
/// [1]: https://en.wikipedia.org/wiki/Shunting-yard_algorithm
|
||||
fn parse(tokens: &[Token]) -> Result<Self, ParseError> {
|
||||
/// Parses an operand:
|
||||
///
|
||||
/// ```
|
||||
/// operand := ⟨num⟩ | "(" ⟨expr⟩ ")"
|
||||
/// ```
|
||||
///
|
||||
/// Note that it's usually nicer to build subslices of a slice instead
|
||||
/// of passing indices manually. But in this case it makes sense to
|
||||
/// work with "global" indices, because this function also returns an
|
||||
/// index into the slice.
|
||||
fn parse_operand(tokens: &[Token], start: usize)
|
||||
-> Result<(Expr, usize), ParseError>
|
||||
{
|
||||
// At this point, we expect an operand, so there need to be some
|
||||
// tokens left
|
||||
if start >= tokens.len() {
|
||||
return Err(ParseError::UnexpectedEof);
|
||||
}
|
||||
|
||||
match tokens[start] {
|
||||
// The operand is just a literal number
|
||||
Token::Number(n) => Ok((Expr::Literal(n), start + 1)),
|
||||
|
||||
// The operand is some operation
|
||||
Token::ParenOpen => {
|
||||
match parse_expr(tokens, start + 1) {
|
||||
Err(e) => Err(e),
|
||||
Ok((expr, after_expr)) => {
|
||||
// We expect the closing paren after the parsed
|
||||
// expression
|
||||
if tokens.get(after_expr) != Some(&Token::ParenClose) {
|
||||
return Err(ParseError::UnmatchedParens);
|
||||
}
|
||||
Ok((expr, after_expr + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An operand starts either with a number or an opening paren
|
||||
tok => Err(ParseError::UnexpectedToken(tok)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an expression:
|
||||
///
|
||||
/// ```
|
||||
/// expr := ⟨operand⟩ [⟨op⟩ ⟨operand⟩]
|
||||
/// ```
|
||||
///
|
||||
fn parse_expr(tokens: &[Token], start: usize)
|
||||
-> Result<(Expr, usize), ParseError>
|
||||
{
|
||||
// First: parse the left hand side (lhs).
|
||||
let (lhs, after_lhs) = match parse_operand(tokens, start) {
|
||||
Err(e) => return Err(e),
|
||||
Ok(tuple) => tuple,
|
||||
};
|
||||
|
||||
// If the lhs consumed all of our tokens, the LHS is the only
|
||||
// expression -> return it. Otherwise we expect an operator.
|
||||
let op = match tokens.get(after_lhs) {
|
||||
Some(&Token::Plus) => Op::Add,
|
||||
Some(&Token::Minus) => Op::Subtract,
|
||||
_ => return Ok((lhs, after_lhs)),
|
||||
};
|
||||
|
||||
// Parse the rhs.
|
||||
let (rhs, after_rhs) = match parse_operand(tokens, after_lhs + 1) {
|
||||
Err(e) => return Err(e),
|
||||
Ok(tuple) => tuple,
|
||||
};
|
||||
|
||||
Ok((
|
||||
Expr::Op {
|
||||
operator: op,
|
||||
operands: Box::new([lhs, rhs]),
|
||||
},
|
||||
after_rhs,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
// Check for special case: no tokens.
|
||||
if tokens.is_empty() {
|
||||
return Err(ParseError::Empty);
|
||||
}
|
||||
|
||||
// Start parsing the token stream as ⟨expr⟩
|
||||
let (expr, after_expr) = match parse_expr(&tokens, 0){
|
||||
Err(e) => return Err(e),
|
||||
Ok(t) => t,
|
||||
};
|
||||
|
||||
// Check if tokens are left (shouldn't happen) and return an error
|
||||
// if that is the case.
|
||||
if after_expr < tokens.len() {
|
||||
return Err(ParseError::UnexpectedAdditionalTokens);
|
||||
}
|
||||
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
/// Evaluates the expression tree to calculate the final result. If an
|
||||
/// overflow occured, None is returned.
|
||||
fn evaluate(&self) -> Option<Integer> {
|
||||
match *self {
|
||||
Expr::Literal(value) => Some(value),
|
||||
Expr::Op { operator, ref operands } => {
|
||||
let lhs = match operands[0].evaluate() {
|
||||
None => return None,
|
||||
Some(lhs) => lhs,
|
||||
};
|
||||
|
||||
let rhs = match operands[1].evaluate() {
|
||||
None => return None,
|
||||
Some(rhs) => rhs,
|
||||
};
|
||||
|
||||
operator.apply(lhs, rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A token in the input stream.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Token {
|
||||
Plus,
|
||||
Minus,
|
||||
ParenOpen,
|
||||
ParenClose,
|
||||
Number(Integer),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum LexError {
|
||||
InvalidChar(char),
|
||||
Overflow,
|
||||
}
|
||||
|
||||
/// Tokenizes the string and returns a list of tokens, if the input is valid.
|
||||
fn tokenize(input: &str) -> Result<Vec<Token>, LexError> {
|
||||
let mut out = Vec::new();
|
||||
let mut current_number = String::new();
|
||||
|
||||
for c in input.chars() {
|
||||
// Check if we were reading a number and reached the end of it
|
||||
if !current_number.is_empty() && !(c >= '0' && c <= '9') {
|
||||
let num: Integer = match current_number.parse() {
|
||||
// The string contains valid digits only, so "overflow" is the
|
||||
// only reason `parse()` would error.
|
||||
Err(_) => return Err(LexError::Overflow),
|
||||
Ok(v) => v,
|
||||
};
|
||||
out.push(Token::Number(num));
|
||||
current_number.clear();
|
||||
}
|
||||
|
||||
let token = match c {
|
||||
'+' => Token::Plus,
|
||||
'-' => Token::Minus,
|
||||
'(' => Token::ParenOpen,
|
||||
')' => Token::ParenClose,
|
||||
c @ '0' ... '9' => {
|
||||
// We do not yet generate a token, but only push the char on
|
||||
// the number buffer
|
||||
current_number.push(c);
|
||||
continue;
|
||||
},
|
||||
c if c.is_whitespace() => continue,
|
||||
c => return Err(LexError::InvalidChar(c)),
|
||||
};
|
||||
out.push(token);
|
||||
}
|
||||
|
||||
// If the last token is a number, we still have to push it
|
||||
if !current_number.is_empty() {
|
||||
let num: Integer = match current_number.parse() {
|
||||
// The string contains valid digits only, so "overflow" is the
|
||||
// only reason `parse()` would error.
|
||||
Err(_) => return Err(LexError::Overflow),
|
||||
Ok(v) => v,
|
||||
};
|
||||
out.push(Token::Number(num));
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
// Read input from the user and just do nothing when the input is empty
|
||||
let input = read_string();
|
||||
if input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to tokenize the input. If it fails, print error message and
|
||||
// start anew.
|
||||
let tokens = match tokenize(&input) {
|
||||
Err(e) => {
|
||||
println!("{:?}", e);
|
||||
continue;
|
||||
}
|
||||
Ok(tokens) => tokens,
|
||||
};
|
||||
|
||||
// Debug output
|
||||
// println!("{:?}", tokens);
|
||||
|
||||
// Try to parse the tokenstream into an expression. If it fails,
|
||||
// print the error and start anew.
|
||||
let expr = match Expr::parse(&tokens) {
|
||||
Err(e) => {
|
||||
println!("error: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
Ok(expr) => expr,
|
||||
};
|
||||
|
||||
// Debug output
|
||||
// println!("{:?}", expr);
|
||||
|
||||
// Evaluate the expression and print the result
|
||||
match expr.evaluate() {
|
||||
None => println!("Overflow occured!"),
|
||||
Some(res) => println!("{}", res),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Reads a string from the user (with a nice prompt).
|
||||
fn read_string() -> String {
|
||||
use std::io::Write;
|
||||
|
||||
// Print prompt
|
||||
print!("calc > ");
|
||||
std::io::stdout().flush().unwrap();
|
||||
|
||||
// Read line
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut buffer)
|
||||
.expect("something went horribly wrong...");
|
||||
|
||||
// Discard trailing newline
|
||||
let new_len = buffer.trim_right().len();
|
||||
buffer.truncate(new_len);
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn parser_valid() {
|
||||
use Token::*;
|
||||
|
||||
// "3"
|
||||
assert_eq!(
|
||||
Expr::parse(&[Number(3)]).unwrap(),
|
||||
Expr::Literal(3)
|
||||
);
|
||||
|
||||
// "(3)"
|
||||
assert_eq!(
|
||||
Expr::parse(&[ParenOpen, Number(3), ParenClose]).unwrap(),
|
||||
Expr::Literal(3)
|
||||
);
|
||||
|
||||
// "3 + 4"
|
||||
assert_eq!(
|
||||
Expr::parse(&[Number(3), Plus, Number(4)]).unwrap(),
|
||||
Expr::Op {
|
||||
operator: Op::Add,
|
||||
operands: Box::new([
|
||||
Expr::Literal(3),
|
||||
Expr::Literal(4),
|
||||
]),
|
||||
}
|
||||
);
|
||||
|
||||
// "(3 + 4)"
|
||||
assert_eq!(
|
||||
Expr::parse(&[ParenOpen, Number(3), Plus, Number(4), ParenClose]).unwrap(),
|
||||
Expr::Op {
|
||||
operator: Op::Add,
|
||||
operands: Box::new([
|
||||
Expr::Literal(3),
|
||||
Expr::Literal(4),
|
||||
]),
|
||||
}
|
||||
);
|
||||
|
||||
// "(3) + 4"
|
||||
assert_eq!(
|
||||
Expr::parse(&[ParenOpen, Number(3), ParenClose, Plus, Number(4)]).unwrap(),
|
||||
Expr::Op {
|
||||
operator: Op::Add,
|
||||
operands: Box::new([
|
||||
Expr::Literal(3),
|
||||
Expr::Literal(4),
|
||||
]),
|
||||
}
|
||||
);
|
||||
|
||||
// "(3 - 7) + 4"
|
||||
assert_eq!(
|
||||
Expr::parse(&[
|
||||
ParenOpen, Number(3), Minus, Number(7), ParenClose, Plus, Number(4)
|
||||
]).unwrap(),
|
||||
Expr::Op {
|
||||
operator: Op::Add,
|
||||
operands: Box::new([
|
||||
Expr::Op {
|
||||
operator: Op::Subtract,
|
||||
operands: Box::new([
|
||||
Expr::Literal(3),
|
||||
Expr::Literal(7),
|
||||
]),
|
||||
},
|
||||
Expr::Literal(4),
|
||||
]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_invalid() {
|
||||
use Token::*;
|
||||
|
||||
// ""
|
||||
assert!(Expr::parse(&[]).is_err());
|
||||
|
||||
// "-"
|
||||
assert!(Expr::parse(&[Minus]).is_err());
|
||||
|
||||
// "1 +"
|
||||
assert!(Expr::parse(&[Number(1), Plus]).is_err());
|
||||
|
||||
// "1 + 2 + 3"
|
||||
assert!(Expr::parse(&[Number(1), Plus, Number(2), Plus, Number(3)]).is_err());
|
||||
|
||||
// "(1"
|
||||
assert!(Expr::parse(&[ParenOpen, Number(1)]).is_err());
|
||||
|
||||
// "1)"
|
||||
assert!(Expr::parse(&[Number(1), ParenClose]).is_err());
|
||||
}
|
15
aufgaben/sheet04/task1/README.md
Executable file
15
aufgaben/sheet04/task1/README.md
Executable file
@ -0,0 +1,15 @@
|
||||
Aufgabe 4.1: Programm schöner machen
|
||||
====================================
|
||||
|
||||
In dieser Aufgabe dürft *ihr* mal die Aufgabe der Tutoren übernehmen. Gegeben
|
||||
ist ein Programm, das funktioniert und das tut, was es soll. Es gibt alle
|
||||
Zahlen zwischen 1 und 20, die sowohl eine Primzahl als auch eine ["fröhliche"
|
||||
Zahl](https://en.wikipedia.org/wiki/Happy_number) sind, aus.
|
||||
|
||||
Jedoch ist der Quellcode alles andere als hübsch. Eure Aufgabe ist es, den
|
||||
Quellcode zu verbessern. D.h., ihr sollt die Funktionsweise des Programms gar
|
||||
nicht verändern, sondern nur den Quellcode.
|
||||
|
||||
Versucht, den Code möglichst kurz, gut lesbar und idiomatisch zu gestalten.
|
||||
So viel sei gesagt: Es gibt *sehr viele* Stellen, an denen etwas verbessert
|
||||
werden kann! Versucht möglichst, alle dieser Stellen zu finden.
|
69
aufgaben/sheet04/task1/bad.rs
Executable file
69
aufgaben/sheet04/task1/bad.rs
Executable file
@ -0,0 +1,69 @@
|
||||
// print if they are both
|
||||
fn main() -> () {
|
||||
for iterationnumber in &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] {
|
||||
let iterationnumber = *iterationnumber;
|
||||
if !happy_prime(iterationnumber) {} else {
|
||||
print!("{}", iterationnumber);
|
||||
println!(" is a happy prime!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// is it botH?
|
||||
fn happy_prime(n: i32) -> bool {
|
||||
match check_if_number_is_happy(n) {
|
||||
false => return false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if check_if_number_is_prime(n) == true {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Is it a happy number? https://en.wikipedia.org/wiki/Happy_number
|
||||
fn check_if_number_is_happy(number: i32) -> bool {
|
||||
let mut number: i32= number;
|
||||
|
||||
while number > 1 {
|
||||
let mut tmp = 0;
|
||||
while number > 0 {
|
||||
tmp = tmp + (number %10) * (number%10);
|
||||
number = number / 10;
|
||||
}
|
||||
number = tmp;
|
||||
|
||||
// We ended up in a cycle -> not happy
|
||||
if (number == 4) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// is it priem?
|
||||
fn check_if_number_is_prime(n: i32) -> bool {
|
||||
if n == 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if n == 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut teilerGefunden:bool = false;
|
||||
|
||||
let mut teiler:i32= 2;
|
||||
while (teiler < n) {
|
||||
if (n % teiler == 0) {
|
||||
teilerGefunden = true;
|
||||
}
|
||||
teiler = teiler + 1;
|
||||
}
|
||||
|
||||
return !teilerGefunden;
|
||||
}
|
219
aufgaben/sheet04/task2/README.md
Executable file
219
aufgaben/sheet04/task2/README.md
Executable file
@ -0,0 +1,219 @@
|
||||
Aufgaben 4.2: Taschenrechner
|
||||
============================
|
||||
|
||||
In dieser Aufgabe sollt ihr ein Programm schreiben, welches einfache
|
||||
Rechenaufgaben lösen kann.
|
||||
Zwar kann man sich einen sehr einfachen Rechner schnell manuell
|
||||
zusammenschreiben.
|
||||
Das Programm dieser Aufgabe soll jedoch nicht quick'n'dirty geschrieben werden,
|
||||
sondern später einfach zu erweitern sein.
|
||||
|
||||
Ein Teil des Codes ist bereits in `calculator.rs` vorgegeben.
|
||||
Das gegebene Programm liest in einer Endlosschleife Eingaben vom Nutzer ein,
|
||||
tut damit aber bislang noch nichts, außer sie auszugeben.
|
||||
Das Programm kann man bekanntlich mit Strg+C beenden.
|
||||
|
||||
Das Programm soll später ungefähr folgende Aufgaben lösen können:
|
||||
|
||||
```
|
||||
calc > 3 + 3
|
||||
6
|
||||
calc > 2-3
|
||||
-1
|
||||
calc > 9 + (7 - 2)
|
||||
14
|
||||
calc > 2
|
||||
2
|
||||
calc > (3 - 2) + ((3 - 2) + 9)
|
||||
11
|
||||
```
|
||||
|
||||
|
||||
a) Tokenization
|
||||
---------------
|
||||
|
||||
Die ersten beiden Schritte fast aller Compiler jeglicher Art sind:
|
||||
*Tokenization* (auch *Lexing*) und *Parsing*.
|
||||
Gerade Compiler haben einen extrem komplizierten und komplexen Job.
|
||||
Um dieser Komplexität Herr zu werden, werden eine Reihe von unterschiedlichen
|
||||
Abstraktionsstufen genutzt.
|
||||
Der Quellcode wird permanent von einer Darstellung in die nächste überführt;
|
||||
Analysen, wie Typechecking ("werden alle Typen korrekt benutzt?"), werden nie
|
||||
direkt auf dem Quellcode-String, sondern auf abstrakteren Darstellungen
|
||||
ausgeführt.
|
||||
|
||||
Glücklicherweise müsst ihr keinen Compiler schreiben, sondern nur einen
|
||||
Taschenrechner.
|
||||
Aber auch dabei müssen Nutzereingaben mit einer bestimmten Syntax analysiert
|
||||
werden.
|
||||
Daher werden wir die Funktionalität des Rechners ebenfalls in drei Schritte
|
||||
unterteilen:
|
||||
|
||||
1. Tokenization/Lexing
|
||||
2. Parsing
|
||||
3. Errechnen des Ergebnisses
|
||||
|
||||
Im ersten Schritt wird der Eingabestring in eine Liste von sog. *Tokens*
|
||||
umgewandelt. Ein Token kann ein Zeichen repräsentieren (wie z.B. `"+"`) oder
|
||||
auch mehrere (wie z.B. `"127"`).
|
||||
Anstatt also eine Liste von Zeichen zu betrachten, betrachtet man eine Liste
|
||||
von atomaren Elementen im Code.
|
||||
Um mal ein Beispiel zu geben:
|
||||
|
||||
```
|
||||
Eingabe: "23 + 7"
|
||||
|
||||
| | |
|
||||
Tokens: [Number(23), Plus, Number(7)]
|
||||
```
|
||||
|
||||
Eure erste Aufgabe ist es, einen Typen `Token `zu definieren, der einen Token
|
||||
darstellt.
|
||||
Der Tokenization-Schritt produziert nämlich dann `Vec<Token>`.
|
||||
Wie oben schon gezeigt, muss der Rechner in der Lage sein, Addition und
|
||||
Subtraktion von beliebigen positiven Ganzzahlen auszuführen (das Ergebnis kann
|
||||
negativ sein, nur die einzelnen Zahlen in der Eingabe nicht).
|
||||
Außerdem müssen Klammerausdrücke behandelt werden.
|
||||
Entwerft entsprechend den `Token` Typ.
|
||||
|
||||
---
|
||||
|
||||
Nachdem der Typ definiert ist, müsst ihr jetzt eine Funktion `tokenize()`
|
||||
schreiben.
|
||||
Diese Funktion bekommt einen String und erzeugt daraus eine Liste von Tokens.
|
||||
Natürlich können schon bei der Tokenization Fehler im Input auffallen.
|
||||
Diese Fehler sollten nicht einfach ignoriert werden.
|
||||
|
||||
Die Funktion soll außerdem Whitespaces in der Eingabe komplett ignorieren, dafür
|
||||
also keinen Token erstellen (das würde in späteren Schritten nur ablenken).
|
||||
|
||||
*Wichtig*: Ihr müsst lediglich mit Zahlen umgehen können, die nur
|
||||
*eine* Ziffer besitzen.
|
||||
Zahlen bestehend aus mehreren Ziffern zu lexen, ist -- selbst mit Hilfe von
|
||||
der `parse()` Funktion aus der Standardbibliothek -- nicht ganz trivial.
|
||||
Wer das trotzdem schafft, wird natürlich vom Tutor gelobt ;-)
|
||||
|
||||
|
||||
Schließlich soll die Nutzereingabe an `tokenize()` gegeben werden und das
|
||||
Ergebnis zu Debugzwecken in `main()` ausgegeben werden.
|
||||
Folgende Eingaben können getestet werden:
|
||||
|
||||
- `3`
|
||||
- `3 + 4`
|
||||
- `3 - (7 - 7)`
|
||||
- `3 + ) - (` -> (in diesem Schritt führt das *noch nicht* zum Fehler!)
|
||||
- `peter` -> Hier sollte ein Fehler ausgegeben werden (nicht panic!)
|
||||
|
||||
**Tipps**:
|
||||
|
||||
- Es lohnt sich, die Dokumentation von
|
||||
[`char`](https://doc.rust-lang.org/std/primitive.char.html) durchzulesen
|
||||
- Um Instanzen selbstdefinierter Typen miteinander vergleichen zu können, kann
|
||||
man `#[derive(PartialEq)]` über die Typdefinition schreiben. Dann kann der
|
||||
Typ per `==` und `!=` verglichen werden.
|
||||
|
||||
b) Definition des Parsetrees
|
||||
----------------------------
|
||||
|
||||
Bisher haben wir aus einem String einen Tokenstream gemacht.
|
||||
Im Parsing-Schritt wird aus dem Tokenstream ein sog. Parsetree erzeugt.
|
||||
Doch bevor wir uns an das Parsing wagen, müssen wir erstmal Typen definieren,
|
||||
um den Parsetree darzustellen.
|
||||
|
||||
Hier ist mal ein Beispiel eines Parsetrees zu der Eingabe `3 + (6 - 1)` zu
|
||||
sehen:
|
||||
|
||||
```
|
||||
+---------+
|
||||
| Summe |
|
||||
+---------+
|
||||
/ \
|
||||
/ \
|
||||
+-----+ +-----------+
|
||||
| 3 | | Differenz |
|
||||
+-----+ +-----------+
|
||||
/ \
|
||||
/ \
|
||||
+-----+ +-----+
|
||||
| 6 | | 1 |
|
||||
+-----+ +-----+
|
||||
```
|
||||
|
||||
Wir sehen also:
|
||||
|
||||
- Blätter des Baumes stellen die Literale/eingegebenen Zahlen dar
|
||||
- Innere Knoten des Baumes wissen, welche Art von Rechenoperation sie
|
||||
darstellen und welche Kinder sie besitzen
|
||||
|
||||
Erstellt zunächst einen Typ `Op`, welcher eine Rechenoperation darstellt.
|
||||
Dieser Typ soll dann eine Methode `apply()` bekommen.
|
||||
Diese Methode bekommt zwei Zahlen und gibt das Ergebnis der Operation
|
||||
angewendet auf diese beiden Zahlen zurück.
|
||||
|
||||
Danach soll der Typ `Expr` erstellt werden, der letztendlich den Parsetree
|
||||
darstellt.
|
||||
*Tipp*: Auch wenn wir immer nur zwei Kinder pro Knoten erwarten, könnt ihr hier
|
||||
gerne `Vec<_>` benutzen!
|
||||
|
||||
Dieser Typ soll eine Methode `evaluate()` bekommen, welche den ganzen Baum
|
||||
auswertet (also das Ergebnis ausrechnet).
|
||||
Die Methode würde also auf dem oben gezeigten Baum "8" zurückgeben.
|
||||
Das ist also Schritt 3 aus der oben genannten Liste von Schritten.
|
||||
|
||||
Baut euch in einer kleinen Hilfsfunktion manuell eine Instanz (z.B. den oben
|
||||
gezeigten Baum) von `Expr` zusammen und testet `evaluate()` auf dieser Instanz.
|
||||
Ruft diese Hilfsfunktion in `main()` auf, um das Ergebnis zu prüfen.
|
||||
Löscht diese Hilfsfunktion auch nicht, sodass euer Tutor sie später nutzen
|
||||
kann.
|
||||
|
||||
|
||||
c) Parsing
|
||||
----------
|
||||
|
||||
Dieser zweite Schritt ist der komplizierteste.
|
||||
Ihr müsst diese Teilaufgabe nicht komplett lösen, da die komplette Lösung
|
||||
nicht gerade trivial ist.
|
||||
Mindestanforderung ist jedoch, dass ihr die folgenden Eingaben parsen könnt:
|
||||
|
||||
- `3`
|
||||
- `1 + 2`
|
||||
- `9 - 6`
|
||||
|
||||
Das Parsing soll in einer Konstruktorfunktion von `Expr` geschehen.
|
||||
Nennen wir diese Funktion einfach `parse()`.
|
||||
Die Funktion bekommt natürlich den vorher generierten Tokenstream und soll
|
||||
bei Erfolg eine `Expr`-Instanz zurückgeben, welche den Parsetree darstellt.
|
||||
|
||||
Aber natürlich kann das Parsing auch auf Fehler in der Eingabe stoßen.
|
||||
Zum Beispiel sollten Fehler wie eine leere Eingabe und eine falsche Syntax
|
||||
gemeldet werden.
|
||||
|
||||
Ein Hinweis zur Syntax: Wir gehen davon aus, dass jeder Knoten im Baum nur zwei
|
||||
Kinder hat und Rechnungen immer geklammert sind.
|
||||
Eine Eingabe von `3 + 4 + 5` ist also ungültig und müsste als `3 + (4 + 5)`
|
||||
oder `(3 + 4) + 5` eingegeben werden.
|
||||
Das vereinfacht das Parsing stark.
|
||||
Unsere Grammatik ist also:
|
||||
|
||||
```
|
||||
expr := ⟨operand⟩ [⟨op⟩ ⟨operand⟩]
|
||||
operand := ⟨num⟩ | "(" ⟨expr⟩ ")"
|
||||
op := "+" | "-"
|
||||
num := "0" | "1" | ... | "9"
|
||||
```
|
||||
|
||||
*Tipps*:
|
||||
|
||||
- Wenn ihr versucht, den kompletten Parser zu implementieren, bietet
|
||||
sich natürlich eine Art von Rekursion an.
|
||||
- Es gibt recht viele Wege, diesen Parser zu implementieren. Ein Weg, den ich
|
||||
recht bequem finde: Man speichert sich den aktuellen Index im Tokenstream
|
||||
und erhöht diesen immer, wenn man weitere Tokens verarbeitet hat.
|
||||
- Kleine Hilfsfunktionen können doppelten Code vermeiden.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Zuletzt (unabhängig davon, ob ihr nur den halben oder ganzen Parser
|
||||
implementiert habt) soll alles in `main()` benutzt werden, um den
|
||||
Taschenrechner funktionsfähig zu machen.
|
35
aufgaben/sheet04/task2/calculator.rs
Executable file
35
aufgaben/sheet04/task2/calculator.rs
Executable file
@ -0,0 +1,35 @@
|
||||
|
||||
fn main() {
|
||||
loop {
|
||||
// Read input from the user and just do nothing when the input is empty
|
||||
let input = read_string();
|
||||
if input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Debug output
|
||||
println!("{}", input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Reads a string from the user (with a nice prompt).
|
||||
fn read_string() -> String {
|
||||
use std::io::Write;
|
||||
|
||||
// Print prompt
|
||||
print!("calc > ");
|
||||
std::io::stdout().flush().unwrap();
|
||||
|
||||
// Read line
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut buffer)
|
||||
.expect("something went horribly wrong...");
|
||||
|
||||
// Discard trailing newline
|
||||
let new_len = buffer.trim_right().len();
|
||||
buffer.truncate(new_len);
|
||||
|
||||
buffer
|
||||
}
|
Reference in New Issue
Block a user