- Added assume_no_enum() because the Err enum is used at least as often as [] for reporting fails.

- changed substring(a b) behavior from "b is the max length of the resulting string" to "b is the exclusive end index, unless it is negative, in which case its abs() value is the maximum length".
- fixed a bug in libs/path
- added the http_requests library, which can be used to make very basic GET requests
- fixed a bug in the gui library that would mess up button handling
- you can now escape comments using a backslash `\`: \// will turn into the literal //, \/* will turn into the literal /*. Useful for URLs (because comments work in string literals). Putting a backslash before a linebreak will also ignore that linebreak (useful in long string literals)
This commit is contained in:
Dummi26 2023-04-13 03:04:47 +02:00
parent 2acdcd3f53
commit ce61749260
15 changed files with 1600 additions and 179 deletions

View File

@ -2,6 +2,17 @@
Mers is an experimental programming language inspired by high-level and scripting languages, but with error handling inspired by rust.
## WARNING
If you use libraries, be aware that they run as a seperate process that might not exit with mers!
This means that, after running a script 40-50 times (which can happen more quickly than you might realize),
you might find 40-50 random processes just running and possibly maxing our your cpu.
So if you use libraries (recommendation: don't, the implementation is pretty bad anyway. just use any other language), make sure to kill those processes once you're done
until I figure out how to make that happen automatically.
(I believe the issue happens when closing the window from the GUI library, which crashes mers, leaving the http library process running)
(other than that, the language is pretty usable, i promise...)
## Features
### Multiple Types

View File

@ -5,10 +5,7 @@ use std::{
use crate::{
libs::DirectReader,
script::{
val_data::{VData, VDataEnum},
val_type::VType,
},
script::{val_data::VData, val_type::VType},
};
use super::{data_from_bytes, data_to_bytes};
@ -38,8 +35,8 @@ impl MyLib {
MyLibTaskCompletion { _priv: () },
)
}
pub fn get_enum(&self, e: &str) -> usize {
*self.enum_variants.get(e).unwrap()
pub fn get_enum(&self, e: &str) -> Option<usize> {
self.enum_variants.get(e).map(|v| *v)
}
pub fn run<I, O>(
&mut self,
@ -84,6 +81,8 @@ impl MyLib {
}
'I' => {
let mut line = String::new();
loop {
line.clear();
stdin.read_line(&mut line).unwrap();
if let Some((task, args)) = line.split_once(' ') {
match task {
@ -95,8 +94,14 @@ impl MyLib {
}
_ => todo!(),
}
} else {
match line.trim_end() {
"init_finished" => break,
_ => unreachable!(),
}
None
}
}
Some(MyLibTask::FinishedInit(MyLibTaskCompletion { _priv: () }))
}
'f' => {
let fnid = stdin.one_byte().unwrap() as usize;
@ -119,6 +124,7 @@ impl MyLib {
pub enum MyLibTask {
None(MyLibTaskCompletion),
FinishedInit(MyLibTaskCompletion),
RunFunction(MyLibTaskRunFunction),
}
pub struct MyLibTaskRunFunction {

View File

@ -32,7 +32,8 @@ the identifying ascii chars:
f a function: followed by the function signature, i.e. "my_func(string int/float [string]) string/[float int]"
x end: indicates that you are done registering things
I init 2
TODO! (currently nothing)
can send some tasks,
must end with a line saying 'init_finished'.
reply should be a single line (only the newline char). If there is data before the newline char, it will be reported as an error and the script will not run.
f call a function:
followed by the function id byte (0 <= id < #funcs; function ids are assigned in ascending order as they were registered in the reply to "i"
@ -124,9 +125,11 @@ impl Lib {
_ => todo!(),
}
}
write!(stdin, "I").unwrap();
for (enum_name, enum_id) in enum_variants.iter() {
writeln!(stdin, "Iset_enum_id {enum_name} {enum_id}").unwrap();
writeln!(stdin, "set_enum_id {enum_name} {enum_id}").unwrap();
}
writeln!(stdin, "init_finished").unwrap();
Ok(Self {
process: handle,
stdin: Arc::new(Mutex::new(stdin)),

View File

@ -10,7 +10,6 @@ pub fn path_from_string(path: &str, script_directory: &PathBuf) -> Option<PathBu
.unwrap_or_else(|_| script_directory.clone())
.parent()
{
eprintln!("Parent: {:?}", p);
let p = p.join(&path);
if p.exists() {
return Some(p);

View File

@ -32,6 +32,18 @@ impl File {
let mut data = String::with_capacity(data.len());
loop {
match chs.next() {
Some('\\') => match chs.next() {
// backslash can escape these characters:
Some('\n') => data.push('\\'),
// backshash invalidates comments, so \// will just be //.
Some('/') => data.push('/'),
// backslash does nothing otherwise.
Some(ch) => {
data.push('\\');
data.push(ch);
}
None => data.push('\\'),
},
Some('/') => match chs.next() {
Some('/') => loop {
match chs.next() {

View File

@ -20,6 +20,7 @@ pub const EVS: [&'static str; 1] = ["Err"];
pub enum BuiltinFunction {
// core
Assume1, // assume []/[t] is [t], return t. Optionally provide a reason as to why (2nd arg)
AssumeNoEnum, // assume enum(*)/t is t.
NoEnum,
Matches,
// print
@ -83,6 +84,7 @@ impl BuiltinFunction {
pub fn get(s: &str) -> Option<Self> {
Some(match s {
"assume1" => Self::Assume1,
"assume_no_enum" => Self::AssumeNoEnum,
"noenum" => Self::NoEnum,
"matches" => Self::Matches,
"print" => Self::Print,
@ -151,7 +153,7 @@ impl BuiltinFunction {
}
}
if !len0 {
eprintln!("Warn: calling assume1 on a value of type {}, which will never be a length-0 tuple and therefore will not cannot fail.", input[0]);
eprintln!("Warn: calling assume1 on a value of type {}, which will never be a length-0 tuple and therefore cannot fail.", input[0]);
}
if !len1 {
eprintln!("Warn: calling assume1 on a value of type {}, which will always be a length-0 tuple!", input[0]);
@ -169,6 +171,37 @@ impl BuiltinFunction {
false
}
}
Self::AssumeNoEnum => {
if input.len() >= 1 {
let mut someenum = false;
let mut noenum = false;
for t in input[0].types.iter() {
match t {
VSingleType::EnumVariant(..) | VSingleType::EnumVariantS(..) => {
someenum = true
}
_ => noenum = true,
}
}
if !someenum {
eprintln!("Warn: calling assume_no_enum on a value of type {}, which will never be an enum and therefore cannot fail.", input[0]);
}
if !noenum {
eprintln!("Warn: calling assume_no_enum on a value of type {}, which will always be an enum!", input[0]);
}
if input.len() >= 2 {
if input.len() == 2 {
input[1].fits_in(&VSingleType::String.to()).is_empty()
} else {
false
}
} else {
true
}
} else {
false
}
}
Self::NoEnum => input.len() == 1,
Self::Matches => input.len() == 1,
Self::Print | Self::Println => {
@ -341,6 +374,16 @@ impl BuiltinFunction {
}
out
}
Self::AssumeNoEnum => {
let mut out = VType { types: vec![] };
for t in &input[0].types {
match t {
VSingleType::EnumVariant(..) | VSingleType::EnumVariantS(..) => (),
t => out = out | t.clone().to(),
}
}
out
}
Self::NoEnum => input[0].clone().noenum(),
Self::Matches => input[0].matches().1,
// []
@ -557,12 +600,31 @@ impl BuiltinFunction {
}
} else {
String::new()
}
},
);
}
}
v => v.to(),
},
Self::AssumeNoEnum => {
let data = args[0].run(vars, libs);
match data.data {
VDataEnum::EnumVariant(..) => panic!(
"ASSUMPTION FAILED: assume_no_enum :: found {} :: {}",
data,
if args.len() > 1 {
if let VDataEnum::String(v) = args[1].run(vars, libs).data {
v
} else {
String::new()
}
} else {
String::new()
}
),
d => d.to(),
}
}
Self::NoEnum => args[0].run(vars, libs).noenum(),
Self::Matches => match args[0].run(vars, libs).data.matches() {
Some(v) => VDataEnum::Tuple(vec![v]).to(),
@ -1232,25 +1294,32 @@ impl BuiltinFunction {
}
Self::Get => {
if args.len() == 2 {
if let (VDataEnum::Reference(v), VDataEnum::Int(i)) =
if let (container, VDataEnum::Int(i)) =
(args[0].run(vars, libs).data, args[1].run(vars, libs).data)
{
if let VDataEnum::List(_, v) | VDataEnum::Tuple(v) =
&mut v.lock().unwrap().data
{
if i >= 0 {
match v.get(i as usize) {
Some(v) => VDataEnum::Tuple(vec![v.clone()]).to(),
match match container {
VDataEnum::Reference(v) => match &v.lock().unwrap().data {
VDataEnum::List(_, v) | VDataEnum::Tuple(v) => {
v.get(i as usize).map(|v| v.clone())
}
_ => unreachable!(
"get: reference to something other than list/tuple"
),
},
VDataEnum::List(_, v) | VDataEnum::Tuple(v) => {
v.get(i as usize).map(|v| v.clone())
}
_ => unreachable!("get: not a reference/list/tuple"),
} {
Some(v) => VDataEnum::Tuple(vec![v]).to(),
None => VDataEnum::Tuple(vec![]).to(),
}
} else {
VDataEnum::Tuple(vec![]).to()
}
} else {
unreachable!("get: not a list/tuple")
}
} else {
unreachable!("get: not a reference and index")
unreachable!("get: not a list/tuple/reference and index")
}
} else {
unreachable!("get: not 2 args")
@ -1347,12 +1416,21 @@ impl BuiltinFunction {
};
let left = if left >= 0 { left as usize } else { 0 };
if let Some(len) = len {
let len = if len >= 0 {
len as usize
if len >= 0 {
VDataEnum::String(
a.chars()
.skip(left)
.take((len as usize).saturating_sub(left))
.collect(),
)
.to()
} else {
todo!("negative len - shorthand for backwards? not sure yet...")
};
VDataEnum::String(a.chars().skip(left).take(len).collect()).to()
// negative end index => max length
VDataEnum::String(
a.chars().skip(left).take(len.abs() as usize).collect(),
)
.to()
}
} else {
VDataEnum::String(a.chars().skip(left).collect()).to()
}

View File

@ -1,14 +1,12 @@
use std::{
collections::HashMap,
io::{self, Read},
rc::Rc,
sync::{mpsc, Arc},
sync::mpsc,
time::Duration,
};
use iced::{
executor, time,
widget::{self, button, column, row, text},
widget::{button, column, row, text},
Application, Command, Element, Renderer, Settings, Subscription, Theme,
};
use mers::{
@ -94,7 +92,7 @@ fn main() {
let mut layout = Layout::Row(vec![]);
loop {
run = match my_lib.run(run, &mut stdin, &mut stdout) {
MyLibTask::None(v) => v,
MyLibTask::None(v) | MyLibTask::FinishedInit(v) => v,
MyLibTask::RunFunction(mut f) => {
let return_value = match f.function {
0 => VDataEnum::List(VSingleType::Int.to(), vec![]).to(),
@ -104,7 +102,7 @@ fn main() {
match recv {
MessageAdv::ButtonPressed(path) => v.push(
VDataEnum::EnumVariant(
my_lib.get_enum("ButtonPressed"),
my_lib.get_enum("ButtonPressed").unwrap(),
Box::new(
VDataEnum::List(VSingleType::Int.to(), path).to(),
),
@ -190,10 +188,10 @@ fn main() {
}
fn layout_from_vdata(my_lib: &MyLib, d: VDataEnum) -> Layout {
let row = my_lib.get_enum("Row");
let col = my_lib.get_enum("Column");
let text = my_lib.get_enum("Text");
let button = my_lib.get_enum("Button");
let row = my_lib.get_enum("Row").unwrap();
let col = my_lib.get_enum("Column").unwrap();
let text = my_lib.get_enum("Text").unwrap();
let button = my_lib.get_enum("Button").unwrap();
if let VDataEnum::EnumVariant(variant, inner_data) = d {
if variant == row {
Layout::Row(vec![])
@ -257,7 +255,7 @@ impl Application for App {
recv: flags.0,
sender: flags.1,
buttons: vec![],
layout: Layout::Row(vec![]),
layout: Layout::Column(vec![]),
},
Command::none(),
)
@ -361,6 +359,7 @@ impl App {
}
}
fn calc_layout_stats(&mut self) {
self.buttons.clear();
Self::calc_layout_stats_rec(&self.layout, &mut vec![], &mut self.buttons)
}
fn calc_layout_stats_rec(

3
mers_libs/http_requests Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd ./http_requests_v1
cargo run --release

View File

@ -1,49 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "http_requests"
version = "0.1.0"
dependencies = [
"mers",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mers"
version = "0.1.0"
dependencies = [
"regex",
]
[[package]]
name = "regex"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"

View File

@ -1,29 +0,0 @@
fn main() {
let (mut my_lib, mut run) = MyLib::new(
"GUI-Iced".to_string(),
(0, 0),
"A basic GUI library for mers.".to_string(),
vec![(
"http_get".to_string(),
vec![VSingleType::String],
VType {
types: vec![VSingleType::Tuple(vec![]), VSingleType::String],
},
)],
);
let mut stdin = std::io::stdin().lock();
let mut stdout = std::io::stdout().lock();
let mut layout = Layout::Row(vec![]);
loop {
run = match my_lib.run(run, &mut stdin, &mut stdout) {
MyLibTask::None(v) => v,
MyLibTask::RunFunction(mut f) => {
let return_value = match f.function {
0 => VDataEnum::List(VSingleType::Int.to(), vec![]).to(),
_ => unreachable!(),
};
f.done(&mut stdout, return_value)
}
}
}
}

1138
mers_libs/http_requests_v1/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[package]
name = "http_requests"
name = "http_requests_v1"
version = "0.1.0"
edition = "2021"
@ -7,3 +7,4 @@ edition = "2021"
[dependencies]
mers = { path = "../../mers/" }
reqwest = { version = "0.11.16", features = ["blocking"] }

View File

@ -0,0 +1,95 @@
use mers::{
libs::inlib::{MyLib, MyLibTask},
script::{
val_data::VDataEnum,
val_type::{VSingleType, VType},
},
};
fn main() {
let (mut my_lib, mut run) = MyLib::new(
"HTTP requests for MERS".to_string(),
(0, 0),
"basic HTTP functionality for mers. warning: this is fully single-threaded.".to_string(),
vec![(
"http_get".to_string(),
vec![VSingleType::String.to()],
VType {
types: vec![
VSingleType::String,
VSingleType::EnumVariantS(
format!("Err"),
VType {
types: vec![
VSingleType::EnumVariantS(
format!("ErrBuildingRequest"),
VSingleType::String.to(),
),
VSingleType::EnumVariantS(
format!("ErrGettingResponseText"),
VSingleType::String.to(),
),
],
},
),
],
},
)],
);
let mut stdin = std::io::stdin().lock();
let mut stdout = std::io::stdout().lock();
let mut err_general = 0;
let mut err_building_request = 0;
let mut err_getting_response_text = 0;
loop {
run = match my_lib.run(run, &mut stdin, &mut stdout) {
MyLibTask::None(v) => v,
MyLibTask::FinishedInit(v) => {
err_general = my_lib.get_enum("Err").unwrap();
err_building_request = my_lib.get_enum("ErrBuildingRequest").unwrap();
err_getting_response_text = my_lib.get_enum("ErrGettingResponseText").unwrap();
v
}
MyLibTask::RunFunction(f) => {
let return_value = match f.function {
0 => {
// http_get
if let VDataEnum::String(url) = &f.args[0].data {
match reqwest::blocking::get(url) {
Ok(response) => match response.text() {
Ok(text) => VDataEnum::String(text).to(),
Err(e) => VDataEnum::EnumVariant(
err_general,
Box::new(
VDataEnum::EnumVariant(
err_getting_response_text,
Box::new(VDataEnum::String(e.to_string()).to()),
)
.to(),
),
)
.to(),
},
Err(e) => VDataEnum::EnumVariant(
err_general,
Box::new(
VDataEnum::EnumVariant(
err_building_request,
Box::new(VDataEnum::String(e.to_string()).to()),
)
.to(),
),
)
.to(),
}
} else {
unreachable!()
}
}
_ => unreachable!(),
};
f.done(&mut stdout, return_value)
}
}
}
}

205
musicdb_remote.txt Executable file
View File

@ -0,0 +1,205 @@
lib mers_libs/http_requests
lib mers_libs/gui
// MusicDB is a private project of mine.
// Basically, a SBC is hooked up to some speakers.
// It has a database of my songs and can play them.
// It can be controlled through HTTP requests, such as
// p- to stop
// p+ to play
// qi to get the queue index / playback position
// l to list all the songs
// ql or qL to list the queue
// ...
// this mers program should work as a very simple gui to remotely control
// the playback of songs. It should be able to add/remove songs to/from the queue,
// to start/stop playback, and to display some information about the current song.
// the url used to connect to the SBC and use the HTTP api
api_url = "http:\//192.168.2.103:26315/api/raw?"
// start the GUI
base = gui_init()
set_title("Mers MusicDB Remote")
playback_controls = base.gui_add(Row: [])
play_button = playback_controls.gui_add(Button: "Play")
stop_button = playback_controls.gui_add(Button: "Stop")
// these functions abstract away the api interactions
// because they aren't pretty.
// feel free to skip past this section and look at the gui code instead
fn make_api_request(to_api string) {
http_get(api_url.add(to_api))
}
fn get_queue_index() {
r = make_api_request("qi")
switch! r {
string {
r.regex("[0-9]").assume_no_enum().get(0).assume1().parse_int().assume1()
}
// return the error
Err(ErrBuildingRequest(string)/ErrGettingResponseText(string)) r
}
}
fn get_db_contents() {
// list only 7 = 1 (title) + 2 (artist) + 4 (album)
r = make_api_request("lo7,")
switch! r {
string {
entries = r.regex("#.*:\n.*\n.*\n.*").assume_no_enum()
// initialize the list with a default value
// because mers doesnt know the inner type without it.
db = [[0 "title" "artist" "album"] ...]
&db.remove(0)
for entry entries {
lines = entry.regex(".*")
switch! lines {
[string] {
index_line = &lines.get(0).assume1()
index = index_line.substring(1 index_line.len().sub(1)).parse_int().assume1()
title = &lines.get(1).assume1().substring(1)
artist = &lines.get(2).assume1().substring(1)
album = &lines.get(3).assume1().substring(1)
&db.push([index title artist album])
}
Err(string) {
println("invalid response from server; 3u123128731289")
exit(1)
}
}
}
db
}
Err(ErrBuildingRequest(string)/ErrGettingResponseText(string)) {
println("couldn't request db from server:")
print(" ")
println(r.to_string())
exit(1)
}
}
}
fn get_queue() {
r = make_api_request("ql")
switch! r {
string {
queue = [0 ...]
&queue.remove(0)
for index r.regex("[0-9]+[ \n]").assume_no_enum() {
&queue.push(index.substring(0 index.len().sub(1)).parse_int().assume1())
}
queue
}
Err(ErrBuildingRequest(string)/ErrGettingResponseText(string)) {
println("couldn't request queue from server:")
print(" ")
println(r.to_string())
exit(1)
}
}
}
fn play() {
r = make_api_request("p+")
switch! r {
// success! but nothing to return.
string []
// return the error
Err(ErrBuildingRequest(string)/ErrGettingResponseText(string)) r
}
}
fn stop() {
r = make_api_request("p-")
switch! r {
// success! but nothing to return.
string []
// return the error
Err(ErrBuildingRequest(string)/ErrGettingResponseText(string)) r
}
}
fn queue_song(song_id int) {
make_api_request("q+{0}".format(song_id))
}
// fetch the database contents (a list of songs)
database = get_db_contents()
// this is where all the songs will be displayed.
songs_gui = base.gui_add(Row: [])
queue_list = songs_gui.gui_add(Column: [])
library = songs_gui.gui_add(Column: [])
limit = 0 // set to 0 to disable library length limit
for entry database {
// <SONG> by <ARTIST> on <ALBUM>
song = library.gui_add(Row: [])
song.gui_add(Button: entry.1)
song.gui_add(Text: "by {0} on {1}".format(entry.2 entry.3))
limit = limit.sub(1)
if limit.eq(0) [[]] else []
}
// regularly update the queue
thread(() {
queue_index = get_queue_index().assume_no_enum("if the server isn't reachable, it's ok to crash")
queue_list_inner = queue_list.gui_add(Column: [])
prev_artist = ""
prev_album = ""
for song_id get_queue() {
this_is_playing = if queue_index.eq(0) { queue_index = -1 true } else if queue_index.gt(0) { queue_index = queue_index.sub(1) false } else { false }
song = &database.get(song_id).assume1()
row = queue_list_inner.gui_add(Row: [])
text = if this_is_playing ">> " else ""
text = text.add(song.1)
if prev_artist.eq(song.2) {
if prev_album.eq(song.3) {
} else {
text = text.add("(on {0})".format(song.3))
}
} else {
text = text.add("(by {0} on {1})".format(song.2 song.3))
}
row.gui_add(Text: text)
[]
}
sleep(10)
queue_list_inner.gui_remove()
})
while {
for event gui_updates() {
switch! event {
ButtonPressed([int]) {
e = event.noenum()
println("Pressed button {0}".format(e.to_string()))
match e {
&e.eq(&play_button) {
println("pressed play.")
play()
}
&e.eq(&stop_button) {
println("pressed stop.")
stop()
}
{
// the button is in a row that is in the library,
// so pop()ing twice gets the library path.
// THIS WILL BREAK IF THE GUI LIBRARY SWITCHES TO IDs INSTEAD OF PATHS!
last = &e.pop().assume1()
slast = &e.pop().assume1()
matches = &e.eq(&library)
&e.push(slast)
&e.push(last)
if matches e else []
} {
// the second to last part of the path is the row within library, which is also the index in the db
song = &database.get(&e.get(e.len().sub(2)).assume1()).assume1()
println("Added song \"{0}\" to queue.".format(song.1))
queue_song(song.0)
}
true println("A different button was pressed (unreachable)")
}
}
}
}
[]
}

View File

@ -1,51 +0,0 @@
lib mers_libs/gui
base = gui_init()
column = base.gui_add(Column: [])
text = column.gui_add(Text: "Welcome to MERS GUI!")
button = column.gui_add(Button: "This is a button.")
second_button = column.gui_add(Button: "This is a second button.")
text_state = -2
second_text = column.gui_add(Text: "press the button above to remove this text!")
while {
for event gui_updates() {
switch! event {
ButtonPressed([int]) {
e = event.noenum()
match e {
&e.eq(&button) println("First button pressed")
&e.eq(&second_button) {
// don't match on text_state because we need to change the original from inside the match statement
state = text_state
match state {
// the first, third, fifth, ... time the button is pressed: remove the text
text_state.mod(2).eq(0) {
if text_state.eq(-2) {
// the first time the button is pressed
text_state = 0
set_title("keep pressing the button!")
}
second_text.gui_remove()
}
// the 2nd, 4th, 6th, ... time the button is pressed: add the text back
text_state.eq(1) second_text = column.gui_add(Text: "i'm back!")
text_state.eq(3) second_text = column.gui_add(Text: "you can't fully get rid of me!")
true {
second_text = column.gui_add(Text: "i always come back")
// restart (set text_state to 0)
text_state = -1
}
}
text_state = text_state.add(1)
}
true println("A different button was pressed (unreachable)")
}
}
}
}
[]
}