From 49f465c44427eb55b4f2738dd0d52e9a47aa5def Mon Sep 17 00:00:00 2001 From: Dummi26 Date: Tue, 25 Apr 2023 20:54:35 +0200 Subject: [PATCH] updated tutor and changed 'while' to 'loop' because while should be while while loop is loop , which is the actual syntax mers uses. --- README.md | 14 +-- mers/src/parse/parse.rs | 4 + mers/src/script/code_runnable.rs | 2 +- mers/src/tutor/base_types.rs | 147 ++++++------------------------- mers/src/tutor/error_handling.rs | 59 +++++++++++++ mers/src/tutor/menu.rs | 4 +- mers/src/tutor/mod.rs | 3 + 7 files changed, 102 insertions(+), 131 deletions(-) create mode 100644 mers/src/tutor/error_handling.rs diff --git a/README.md b/README.md index 2725b55..afd155a 100755 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Let's build a counter app: We start at 0. If the user types '+', we increment th The first thing we will need for this is a loop to prevent the app from stopping after the first user input: - while { + loop { println("...") } @@ -137,7 +137,7 @@ Running this should spam your terminal with '...'. Now let's add a counter variable, read user input and print the status message. counter = 0 - while { + loop { input = read_line() println("The counter is currently at {0}. Type + or - to change it.".format(counter.to_string())) } @@ -145,7 +145,7 @@ Now let's add a counter variable, read user input and print the status message. We can then use `eq(a b)` to check if the input is equal to + or -, and then decide to increase or decrease counter: counter = 0 - while { + loop { input = read_line() if input.eq("+") { counter = counter.add(1) @@ -177,7 +177,7 @@ In fact, `fn difference(a int/float b int/float) if a.gt(b) a.sub(b) else b.sub( Let's replace the if statement from before with a match statement! counter = 0 - while { + loop { input = read_line() match input { input.eq("+") counter = counter.add(1) @@ -199,7 +199,7 @@ Match statements are a lot more powerful than if-else-statements, but this will Loops will break if the value returned in the current iteration matches: i = 0 - res = while { + res = loop { i = i.add(1) i.gt(50) } @@ -209,7 +209,7 @@ This will increment i until it reaches 51. Because `51.gt(50)` returns `true`, `res` will be set to `true`. i = 0 - res = while { + res = loop { i = i.add(1) if i.gt(50) i else [] } @@ -217,7 +217,7 @@ Because `51.gt(50)` returns `true`, `res` will be set to `true`. Because a value of type int matches, we now break with "res: 51". For more complicated examples, using `[i]` instead of just `i` is recommended because `[i]` matches even if `i` doesn't. -A while loop's return type will be the matches of the inner return type. +A loop's return type will be the matches of the inner return type. For for loops, which can also end without a value matching, the return type is the same plus the empty tuple `[]`: diff --git a/mers/src/parse/parse.rs b/mers/src/parse/parse.rs index 60c70cf..134d513 100755 --- a/mers/src/parse/parse.rs +++ b/mers/src/parse/parse.rs @@ -588,6 +588,10 @@ fn parse_statement_adv( .to() } "while" => { + eprintln!("Warn: 'while' is now 'loop'. At some point, this will just be an error instead of a warning."); + break SStatementEnum::Loop(parse_statement(file)?).to(); + } + "loop" => { break SStatementEnum::Loop(parse_statement(file)?).to(); } "switch" | "switch!" => { diff --git a/mers/src/script/code_runnable.rs b/mers/src/script/code_runnable.rs index 2c88d56..9465e69 100644 --- a/mers/src/script/code_runnable.rs +++ b/mers/src/script/code_runnable.rs @@ -195,7 +195,7 @@ impl RStatementEnum { } } Self::Loop(c) => loop { - // While loops will break if the value matches. + // loops will break if the value matches. if let Some(break_val) = c.run(vars, info).data.matches() { break break_val; } diff --git a/mers/src/tutor/base_types.rs b/mers/src/tutor/base_types.rs index 85b6f2e..7692fa2 100644 --- a/mers/src/tutor/base_types.rs +++ b/mers/src/tutor/base_types.rs @@ -18,135 +18,38 @@ pub fn run(tutor: &mut Tutor) { // b = \"some text\" // mers knows: b is a string // sub(a b) // mers knows: it can't subtract a string from an int -// Traditional statically-typed languages achieve this same type-safety: - // C / C++ / Java / C# - // int a = 10; - // int b = 5; - // int c = a - b; - // In C#, we can just use 'var' to automatically infer the types - // var a = 10; - // var b = 5; - // var c = a - b; // all of these are ints, and C# knows this - // Not specifying a type for variables is the default in Rust... - // let a = 10; - // let b = 5; - // let c = a - b; - // ... and Go - // a := 10 - // b := 5 - // c := a - b -// Dynamically-typed languages don't need to know the type of their variables at all: - // JavaScript - // let a = 10 - // let b = 5 - // let c = a - b - // Also JavaScript (c becomes NaN in this example) - // let a = \"some text\" - // let b = 5 - // let c = a - b -// However, there are some things dynamic typing can let us do that static typing can't: - // JavaScript - // let x - // if (condition()) { - // x = 10 - // } else { - // x = \"some string\" - // } - // console.log(x) // we can't know if x is an int or a string, but it will work either way. -// We *could* implement this in Rust: - // enum StringOrInt { - // S(String), - // I(i32), - // } - // impl std::fmt::Display for StringOrInt { - // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // match self { - // Self::S(s) => write!(f, \"{}\", s), - // Self::I(i) => write!(f, \"{}\", i), - // } - // } - // } - // fn main() { - // let x; - // if condition() { - // x = StringOrInt::I(10) - // } else { - // x = StringOrInt::S(format!(\"some string\")); - // } - // println!(\"{}\", x); - // } -// While this works, it's a lot of effort. But don't worry, we can do better! - // Mers (it doesn't let you declare variables without initializing them, but that doesn't really matter for the example) - // x = if condition() { - // 10 - // } else { - // \"some string\" - // } - // println(\"x = {0}\".format(x)) -// Okay, but how can we keep the type-safety of statically typed languages if code like this is valid in mers? -// To figure this out, let's just ask mers for the type of x using 'switch! var {}'. -// (you can always reload and check the top of the file to see mers' full error) +// Just like other statically-typed languages, mers achieves this safety by assigning a certain type to each variable (technically to each statement). +// However, mers' type-system has one quirk that sets it apart from most others: -a = \"this is clearly a string\" -switch! a {} // string - -b = 10 -switch! b {} // int - -// Now comment out the two switch statements, reload the file and look at the error produced by 'switch! x {}'. - -x = if true { - 10 +a = if true { + \"some string\" } else { - \"a string\" + 12 } -switch! x {} +switch! a {} -// Mers doesn't know if x is an int or a string, but it knows that it has to be either of those and can't be anything else. -// And this doesn't even rely on any fancy technology, it's all just based on simple and intuitive rules: -// x is the result of an if-else statement, meaning x's type is just the return types from the two branches combined: int and string -> int/string. -// if we remove the else, x's type would be int/[]: either an int or nothing. -// Instead of using switch! to get a compiler error, you can also look at the types at runtime using the builtin debug() function: -// x.debug() // (or debug(x)) | this outputs 'int/string :: int :: 10' | debug()'s format is: -// the statement's type (this is constant and determined at compile-time) :: the value's type (this can change, but it's always one of the types in the constant type) :: string-representation of the value -// (don't forget to comment out the third switch statement, too. you can't return to the menu otherwise) +// A type in mers can consist of multiple single types: The type of a is 'string/int', because it could be either a string or an int. +// You can see this type mentioned in the error at the top of the file, which shows up because 'switch!' wants us to handle all possible types, +// yet we don't handle any ('{}'). -// By combining multiple-types with tuples, we can express complicated data structures without needing structs or enums: -// [int/float int/float]/int/float // one or two numbers -// [int/float ...] // a list of numbers +// By combining tuples ('[a b c]') with the idea of multiple-types, you can create complex datastructures. +// You effectively have all the power of Rust enums (enums containing values) and structs combined: +// Rust's Option: t/[] or [t]/[] if t can be [] itself +// Rust's Result: T/Err(E) -// Mers does have enums, but they are different from what other languages have. -// Enums are just identifiers that can be used to specify what a certain type is supposed to be used for. -// Consider a function read_file() that wants to return a the file's contents as a string. -// If the file doesn't exist or can't be read, the function should return some sort of error. -// Both of these can be the type string: \"this is my file\" and \"Couldn't read file: Permission denied.\". -// To avoid this ambiguity, we can wrap the error in an enum: -// \"this is my file\" and Err: \"Couldn't read file: Permission denied.\". -// Instead of the function just returning string, it now returns string/Err(string). -// This shows programmers that your function can fail and at the same time tells mers -// that the function returns two different types that both need to be handeled: -fn read_file() { - if false { - // successfully read the file - \"this is my file\" - } else { - Err: \"Couldn't read file: I didn't even try.\" - } -} -file = read_file() -// without switching, I can't get to the file's content: -println(file) // this causes an error! -// using switch! instead of switch forces me to handle all types, including the error path. -switch! file { - string { - println(\"File content: {0}\".format(file)) - } - Err(string) { - println(\"Error! {0}\".format(file.noenum())) - } -} +// The Err(E) is mers' version of an enum. An enum in mers is an identifier ('Err') wrapping a type ('E'). +// They don't need to be declared anywhere. You can just return 'PossiblyWrongValue: 0.33' from your function and mers will handle the rest. +// To access the inner value, you can use the noenum() function: +// result = SomeValueInAnEnum: \"a string\" +// println(result) // error - result is not a string +// println(result.noenum()) // works because result is an enum containing a string -// To return to the menu, fix all the errors in this file. +// the \\S+ regex matches anything but whitespaces +words_in_string = \"some string\".regex(\"\\\\S+\") +switch! words_in_string {} +// Types to cover: [string ...]/Err(string) - If the regex is invalid, regex() will return an error. + +// To return to the menu, fix all compiler errors (comment out all switch! statements). true ")); diff --git a/mers/src/tutor/error_handling.rs b/mers/src/tutor/error_handling.rs new file mode 100644 index 0000000..8ba71f3 --- /dev/null +++ b/mers/src/tutor/error_handling.rs @@ -0,0 +1,59 @@ +use crate::script::val_data::VDataEnum; + +use super::Tutor; + +pub fn run(tutor: &mut Tutor) { + tutor.update(Some(" +// Error handling in mers is inspired by Rust. Errors aren't a language feature, +// they are just a value like any other. Usually, errors have the type Err(string) or Err(SomeOtherEnum(string)), +// but this is still just a value in an enum. +// Because of the type system in mers, errors can just be used and don't need any special language features. +// This part of the mers-tutor isn't as much about error handling as it is about dealing with multiple types when you only want some of them, not all, +// but since error handling is the main use-case for this, it felt like the better title for this section. + +// 1. [t]/[] +// This acts like null/nil in most languages or Option in rust. +// This type indicates either '[]', a tuple of length 0 and therefore no data (null/nil/None) +// or '[t]', a tuple of length 1 - one value. This has to be [t] and not just t because t might be [], which would otherwise cause ambiguity. + +// The type [t]/[] is returned by get(), a function that retrieves an item from a list and returns [] if the index was less than 0 or too big for the given list: +list = [1 2 3 4 5 ...] +first = list.get(0) // = [1] +second = list.get(1) // = [2] +does_not_exist = list.get(9) // = [] + +// To handle the result from get(), we can switch on the type: +switch! first { + [int] \"First element in the list: {0}\".format(first.0.to_string()) + [] \"List was empty!\" +} + +// If we already know that the list isn't empty, we can use assume1(). This function takes a [t]/[] and returns t. If it gets called with [], it will crash your program. +\"First element in the list: {0}\".format(first.assume1().to_string()) + +// 2. t/Err(e) +// This acts like Rust's Result and is used in error-handling. +// This is mainly used by functions that do I/O (fs_* and run_command) and can also be handeled using switch or switch! statements. +// Use switch! or .debug() to see the types returned by these functions in detail. +// If switching is too much effort for you and you would like to just crash the program on any error, +// you can use assume_no_enum() to ignore all enum types: +// - t/Err(e) becomes t +// - int/float/string/Err(e)/Err(a) becomes int/float/string + +// To return to the menu, change the index in list.get() so that it returns a value of type [int] instead of []. +list.get(8) +")); + loop { + match tutor.let_user_make_change().run(vec![]).data { + VDataEnum::Tuple(v) if !v.is_empty() => { + break; + } + other => { + tutor.set_status(format!( + " - Returned {other} instead of a value of type [int]." + )); + tutor.update(None); + } + } + } +} diff --git a/mers/src/tutor/menu.rs b/mers/src/tutor/menu.rs index a99cb90..c3a1750 100644 --- a/mers/src/tutor/menu.rs +++ b/mers/src/tutor/menu.rs @@ -2,7 +2,7 @@ use crate::script::val_data::VDataEnum; use super::Tutor; -pub const MAX_POS: usize = 6; +pub const MAX_POS: usize = 7; pub fn run(mut tutor: Tutor) { loop { @@ -18,6 +18,7 @@ fn go_to() 0 // 4 Variables // 5 Returns // 6 Types +// 7 Error handling go_to() ", @@ -34,6 +35,7 @@ go_to() 4 => super::base_variables::run(&mut tutor), 5 => super::base_return::run(&mut tutor), 6 => super::base_types::run(&mut tutor), + 7 => super::error_handling::run(&mut tutor), _ => unreachable!(), } } diff --git a/mers/src/tutor/mod.rs b/mers/src/tutor/mod.rs index 95b59e0..6a609a8 100644 --- a/mers/src/tutor/mod.rs +++ b/mers/src/tutor/mod.rs @@ -11,6 +11,7 @@ mod base_return; mod base_types; mod base_values; mod base_variables; +mod error_handling; mod menu; pub fn start(spawn_new_terminal_for_editor: bool) { @@ -21,6 +22,8 @@ pub fn start(spawn_new_terminal_for_editor: bool) { // This is an interactive experience. After making a change to this file, // save and then reload it to see the tutor's updates. +// DO NOT save the file twice without reloading because you might overwrite changes made by the tutor, +// which can completely ruin the file's formatting until the next full update (page change)! // To begin, change the following value from false to true: false