Решение на Basic BASIC от Йордан Илиев

Обратно към всички решения

Към профила на Йордан Илиев

Резултати

  • 20 точки от тестове
  • 0 бонус точки
  • 20 точки общо
  • 15 успешни тест(а)
  • 0 неуспешни тест(а)

Код

extern crate core;
use std::io::{self, BufReader, Read, Write, BufRead};
use std::collections::{BTreeMap, HashMap};
use std::collections::btree_map::Iter;
use std::iter::Peekable;
#[derive(Debug)]
pub enum InterpreterError {
IoError(io::Error),
UnknownVariable { name: String },
NotANumber { value: String },
SyntaxError { code: String },
RuntimeError { line_number: u16, message: String },
}
impl InterpreterError {
fn new_unknown_variable(name: &str) -> InterpreterError {
InterpreterError::UnknownVariable { name: name.to_string() }
}
fn new_not_a_number(value: &str) -> InterpreterError {
InterpreterError::NotANumber { value: value.to_string() }
}
fn new_syntax_error(code: &str) -> InterpreterError {
InterpreterError::SyntaxError { code: code.to_string() }
}
fn new_runtime_error(line_number: u16, message: &str) -> InterpreterError {
InterpreterError::RuntimeError { line_number, message: message.to_string()}
}
}
#[derive(Debug)]
enum Command {
Print { value: String },
Read { variable_name: String },
Goto { line: u16 },
If { lhs: String, operator: String, rhs: String, line: u16 }
}
impl Command {
fn new_print(value: &str) -> Command {
Command::Print { value: value.to_string() }
}
fn new_read(variable_name: String) -> Command {
Command::Read { variable_name }
}
fn new_goto(line: u16) -> Command {
Command::Goto { line }
}
fn new_if(val1: &str, operator: &str, val2: &str, line: u16) -> Command {
Command::If {
lhs: val1.to_string(),
operator: operator.to_string(),
rhs: val2.to_string(),
line
}
}
}
#[derive(Debug)]
struct CommandWithLine {
command: Command,
line: u16
}
fn starts_with_uppercase(value: &str) -> bool {
value.chars().next()
.map_or(false, char::is_uppercase)
}
impl CommandWithLine {
fn new(code: &str) -> Result<CommandWithLine, InterpreterError> {
let arguments: Vec<&str> = code.split_whitespace().collect();
let to_u16_or_syntax_err = |str: &str| {
str
.parse::<u16>()
.map_err(|_| InterpreterError::new_syntax_error(code))
};
let starts_with_uppercase_or_syntax_error = |str: &str| {
if starts_with_uppercase(str) {
Ok(str.to_string())
} else {
Err(InterpreterError::new_syntax_error(code))
}
};
let command = match arguments[..] {
[_, "PRINT", value] => Command::new_print(value),
[_, "READ", variable_name] => Command::new_read(
starts_with_uppercase_or_syntax_error(variable_name)?
),
[_, "GOTO", jump_line] => Command::new_goto(
to_u16_or_syntax_err(jump_line)?
),
[_, "IF", lhs, op, rhs, "GOTO", jump_line] => Command::new_if(
lhs,
op,
rhs,
to_u16_or_syntax_err(jump_line)?
),
_ => return Err(InterpreterError::new_syntax_error(code)) //xd, that can be `Err(..)?`
};
let current_line = to_u16_or_syntax_err(arguments[0])?;
Ok(CommandWithLine { command, line: current_line })
}
}
pub struct Interpreter<'a, R: Read, W: Write> {
input: BufReader<R>,
output: &'a mut W,
program_lines: BTreeMap<u16, Command>,
variables: HashMap<String, u16>
}
impl<'a, R: Read, W: Write> Interpreter<'a, R, W> {
pub fn new(input: R, output: &'a mut W) -> Self {
Interpreter {
input: BufReader::new(input),
output,
program_lines: BTreeMap::new(),
variables: HashMap::new()
}
}
}
impl<'a, R: Read, W: Write> Interpreter<'a, R, W> {
pub fn add(&mut self, code: &str) -> Result<(), InterpreterError> {
let CommandWithLine { command, line } = CommandWithLine::new(code)?;
self.program_lines.insert(line, command);
Ok(())
}
pub fn eval_value(&self, value: &str) -> Result<u16, InterpreterError> {
if starts_with_uppercase(value) {
self.variables.get(value)
.cloned()
.ok_or_else(|| InterpreterError::new_unknown_variable(value))
} else {
value
.parse::<u16>()
.map_err(|_| InterpreterError::new_not_a_number(value))
}
}
fn eval_for_print(&self, value: &str) -> Result<String, InterpreterError> {
match self.eval_value(value) {
Ok(number) => Ok(number.to_string()),
Err(InterpreterError::NotANumber { value }) => Ok(value),
Err(something) => Err(something) // is there some better way to change the Ok type?
}
}
fn skip_to_line(line_iter: &mut Peekable<Iter<u16, Command>>, jump_line: u16) -> () {
while line_iter.peek().map(|(k, _)| **k != jump_line).unwrap_or(false) {
line_iter.next();
}
}
pub fn run(&mut self) -> Result<(), InterpreterError> {
let mut line_iterator = self.program_lines.iter().peekable();
while let Some((&line, command)) = line_iterator.next() {
match command {
Command::Print { value } => {
let evaluated = self.eval_for_print(value)
.map_err(|_| InterpreterError::new_runtime_error(line, "Variable does not exist"))?;
writeln!(self.output, "{}", evaluated)
.map_err(|io_err| InterpreterError::IoError(io_err))?
},
Command::Read { variable_name } => {
let mut buf = String::new();
self.input.read_line(&mut buf)
.map_err(|io_err| InterpreterError::IoError(io_err))?;
buf.pop(); // removes '\n'
let parsed_number = buf.parse::<u16>()
.map_err(|_| InterpreterError::new_runtime_error(line, "Entered value is not a number"))?;
self.variables.insert(variable_name.clone(), parsed_number);
},
Command::Goto { line: jump_line } => {
if self.program_lines.get(jump_line).is_none() {
return Err(InterpreterError::new_runtime_error(line, "Trying to jump to non-existing line"));
}
let mut next_line_iter = self.program_lines.iter().peekable();
Self::skip_to_line(&mut next_line_iter, *jump_line);
line_iterator = next_line_iter;
}
Command::If { lhs, operator, rhs, line: jump_line } => {
let lhs = self.eval_value(lhs)
.map_err(|_| InterpreterError::new_runtime_error(line, "Variable does not exist"))?;
let rhs = self.eval_value(rhs)
.map_err(|_| InterpreterError::new_runtime_error(line, "Variable does not exist"))?;
let should_jump = match operator.as_str() {
"=" => lhs == rhs,
">" => lhs > rhs,
"<" => lhs < rhs,
_ => unimplemented!()
};
if should_jump {
// TODO - copy paste bad
if self.program_lines.get(jump_line).is_none() {
return Err(InterpreterError::new_runtime_error(line, "Trying to jump to non-existing line"));
}
let mut next_line_iter = self.program_lines.iter().peekable();
Self::skip_to_line(&mut next_line_iter, *jump_line);
line_iterator = next_line_iter;
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod my_tests {
use super::*;
macro_rules! assert_match {
($expr:expr, $pat:pat) => {
let result = $expr;
if let $pat = result {
// all good
} else {
assert!(false, "Expression {:?} does not match the pattern {:?}", result, stringify!($pat));
}
}
}
#[test]
fn invalid_add_commands() {
let input: &[u8] = b"1\n2\n";
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
assert!(interpreter.add("").is_err());
assert!(interpreter.add("30").is_err());
assert!(interpreter.add("AB READ A").is_err());
assert!(interpreter.add("READ X").is_err());
assert!(interpreter.add("- READ A").is_err());
assert!(interpreter.add("10 READ alabala").is_err());
assert!(interpreter.add("10 PRINT").is_err());
assert!(interpreter.add("10 PRINT hehe xdd").is_err());
assert!(interpreter.add("10 READ").is_err());
assert!(interpreter.add("10 READ lowercase").is_err());
assert!(interpreter.add("10 READ Uppercase but too much arguments").is_err());
assert!(interpreter.add("10 GOTO").is_err());
assert!(interpreter.add("10 GOTO non-numeric").is_err());
assert!(interpreter.add("10 GOTO 10 20?").is_err());
assert!(interpreter.add("10 GOTO -5").is_err());
assert!(interpreter.add("10 IF val op val GOTO non-numeric").is_err());
assert!(interpreter.add("10 IF val op val no-goto 12").is_err());
assert!(interpreter.add("10 IF val op val GOTO 12 34??").is_err());
assert!(interpreter.add("10 SOMETHING 12").is_err());
assert!(interpreter.add("20 IF 13 > 18 GOTO").is_err());
}
fn happy_path<'a>(input: &'a [u8], expected_output: &'a str, code: Vec<&'a str>) {
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
for line in code {
interpreter.add(line).unwrap();
}
interpreter.run().unwrap();
assert_eq!(String::from_utf8(output).unwrap(), expected_output);
}
#[test]
fn test_print_read() {
happy_path(b"1\n2\n", "1\n2\n", vec![
"10 READ A",
"20 READ B",
"30 PRINT A",
"40 PRINT B"
]);
}
#[test]
fn test_print_read_cyrillic() {
happy_path(b"70\n", "70\nйордан_като_literal\n", vec![
"10 READ Йордан",
"20 PRINT Йордан",
"30 PRINT йордан_като_literal"
]);
}
#[test]
fn test_line_out_of_order() {
happy_path(b"", "first\nsecond\n", vec![
"20 PRINT second",
"10 PRINT first",
]);
}
#[test]
fn test_line_overriding() {
happy_path(b"", "overwriter\n", vec![
"20 PRINT overriden",
"20 PRINT overwriter",
]);
}
#[test]
fn test_print_literal() {
happy_path(b"", "hehe_xd\n", vec![
"10 PRINT hehe_xd",
]);
}
#[test]
fn test_goto() {
happy_path(b"1\n2\n", "1\n", vec![
"10 READ A",
"20 READ B",
"50 GOTO 70",
"60 PRINT B",
"70 PRINT A"
]);
}
#[test]
fn test_if_equals_literals() {
happy_path(b"", "win\n", vec![
"10 IF 1 = 1 GOTO 30",
"20 PRINT loss",
"30 PRINT win"
]);
}
#[test]
fn test_if_inequality() {
happy_path(b"", "win\n", vec![
"10 IF 2 > 1 GOTO 30",
"20 PRINT loss",
"30 PRINT win"
]);
}
#[test]
fn test_if_mixed() {
happy_path(b"3\n", "win\n", vec![
"5 READ XD",
"10 IF XD > 2 GOTO 30",
"20 PRINT loss",
"30 PRINT win"
]);
}
#[test]
fn test_number_guessing() {
happy_path(b"40\n43\n42\n", "guess_a_number\ntoo_low\nguess_a_number\ntoo_high\nguess_a_number\nyou_got_it!\n", vec![
"10 PRINT guess_a_number",
"20 READ Guess",
"30 IF Guess > 42 GOTO 100",
"40 IF Guess < 42 GOTO 200",
"50 IF Guess = 42 GOTO 300",
"100 PRINT too_high",
"110 GOTO 10",
"200 PRINT too_low",
"210 GOTO 10",
"300 PRINT you_got_it!"
]);
}
#[test]
fn test_if_unreachable_invalid_goto() {
happy_path(b"", "", vec![
"10 IF 1 > 2 GOTO 69",
]);
}
fn expect_runtime_err<'a>(input: &'a [u8], expected_line: u16, code: Vec<&'a str>) {
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
for line in code {
interpreter.add(line).unwrap();
}
match interpreter.run().unwrap_err() {
InterpreterError::RuntimeError { line_number, .. }
=> assert_eq!(expected_line, line_number),
_ => panic!("Code didn't return an error, but error was expected!")
}
}
#[test]
fn test_if_undefined_variable() {
expect_runtime_err(b"", 10, vec![
"10 IF 3 > Undefined GOTO 20",
"20 PRINT unreachable"
]);
}
#[test]
fn test_if_string_literal() {
expect_runtime_err(b"", 10, vec![
"10 IF literal > 2 GOTO 20",
"20 PRINT unreachable",
]);
}
#[test]
fn test_if_goto_invalid_jump() {
expect_runtime_err(b"", 10, vec![
"10 IF 3 > 2 GOTO 21",
"20 PRINT unreachable",
]);
}
#[test]
fn test_read_non_numeric() {
expect_runtime_err(b"someting-non-numeric\n", 10, vec![
"10 READ A",
"20 PRINT B",
]);
}
#[test]
fn test_goto_non_existing_line() {
expect_runtime_err(b"1\n2\n", 50, vec![
"10 READ A",
"20 READ B",
"50 GOTO 71",
"60 PRINT B",
"70 PRINT A"
]);
}
// stolen tests
#[test]
fn test_basic_1() {
// Забележете `b""` -- низ от байтове
let input: &[u8] = b"1\n2\n";
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
interpreter.add("10 READ A").unwrap();
interpreter.add("20 READ B").unwrap();
interpreter.add("30 PRINT A").unwrap();
interpreter.add("40 PRINT B").unwrap();
interpreter.run().unwrap();
assert_eq!(interpreter.eval_value("A").unwrap(), 1_u16);
assert_eq!(String::from_utf8(output).unwrap(), "1\n2\n");
}
#[test]
fn test_basic_2() {
let input: &[u8] = b"";
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
assert_match!(interpreter.add("10 PRINT"), Err(InterpreterError::SyntaxError { .. }));
}
struct NotBuffered {}
impl std::io::Read for NotBuffered {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Ok(0)
}
}
#[test]
fn test_not_buffered() {
let input = NotBuffered {};
let mut output = Vec::<u8>::new();
let mut interpreter = Interpreter::new(input, &mut output);
interpreter.add("10 PRINT 10").unwrap();
interpreter.run().unwrap();
}
}

Лог от изпълнението

Compiling solution v0.1.0 (/tmp/d20230111-3772066-1rjsyco/solution)
    Finished test [unoptimized + debuginfo] target(s) in 1.62s
     Running tests/solution_test.rs (target/debug/deps/solution_test-0edbea2040daef01)

running 15 tests
test solution_test::test_basic_if ... ok
test solution_test::test_basic_goto ... ok
test solution_test::test_basic_print ... ok
test solution_test::test_basic_input ... ok
test solution_test::test_erroring_goto ... ok
test solution_test::test_basic_read ... ok
test solution_test::test_io_error_read ... ok
test solution_test::test_full_program ... ok
test solution_test::test_io_error_write ... ok
test solution_test::test_line_order_and_overwriting ... ok
test solution_test::test_print_cyrillic ... ok
test solution_test::test_print_vars_and_strings ... ok
test solution_test::test_syntax_errors_1 ... ok
test solution_test::test_runtime_errors ... ok
test solution_test::test_syntax_errors_2 ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

История (1 версия и 0 коментара)

Йордан качи първо решение на 09.01.2023 17:59 (преди 5 месеца)