updated tutor and changed 'while' to 'loop' because while should be while <condition> <statement> while loop is loop <statement>, which is the actual syntax mers uses.

This commit is contained in:
Dummi26 2023-04-25 20:54:35 +02:00
parent e8ee005743
commit 49f465c444
7 changed files with 102 additions and 131 deletions

View File

@ -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 `[]`:

View File

@ -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!" => {

View File

@ -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;
}

View File

@ -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>: t/[] or [t]/[] if t can be [] itself
// Rust's Result<T, E>: 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
"));

View File

@ -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<T> 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<T, E> 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);
}
}
}
}

View File

@ -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!(),
}
}

View File

@ -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