--- title: "BASIC Interpreter Part 3: Copying values" date: 2023-01-20T16:13:56 slug: basic-interpreter-part-3-copying-values --- Now time for part three of my Sinclair BASIC Interpreter. In [the previous post](https://lewisdale.dev/post/basic-interpreter-part-two-variables/) I added the ability to assign data to a variable using the `LET` command. Now, it's time to use those variables in expressions. That means: - Assigning one variable to another's value (`LET a=b`) - Performing basic mathematical expressions - Assignment using those maths expressions (`LET c=a*b`) ## Assigning variables First things first, I need to update the enum I have for storing variables. Right now, I use an enum called Primitive, which has either an `Int (i64)` or a `String` option. To begin with, I'm going to try adding a third branch to the logic, called `Assignment`, which will also store a String - in this case it'll be a variable name. I'll also add a test to demonstrate this, which naturally fails right now (yay TDD). ```rust #[derive(Debug, PartialEq, Eq, Clone)] pub enum Primitive { Int(i64), String(String), Assignment(String) } #[test] fn it_assigns_one_variable_to_another() { let line = 10 LET a=b; let (_, result) = parse_line(line).unwrap(); let expected: Line = ( 10, Command::Var(( String::from(a), Primitive::Assignment(String::from(b)) )) ); assert_eq!(result, expected); } ``` So as a first pass, I just want to assume that everything else is correct in the line, and whatever is on the other side is a variable name. So, with a small amount of validation (that the first character isn't a digit), I'm just using `take_until1`, separated by the equals sign, to collect everything as a String: ```rust fn parse_assignment(i: &str) -> IResult<&str, (String, Primitive)> { let (i, _) = not(digit1)(i)?; let (i, id) = take_until1(=)(i)?; let (i, _) = tag(=)(i)?; Ok((i, (id.to_string(), Primitive::Assignment(i.to_string())))) } fn parse_var(i: &str) -> IResult<&str, (String, Primitive)> { alt((parse_int, parse_str, parse_assignment))(i) } ``` This is extremely permissive in it's current form, so it needs to go at the very end of the `alt` combinator. But, it works - well, it passes the one test. But, when I run my entire test suite I find it's causes a regression. The parse should not accept strings with multi-character names, but this parser is permissive enough that it passes. So, the next thing to do is to properly validate the variable name. ```rust // Take everything until it hits a newline, if it does fn consume_line(i: &str) -> IResult<&str, &str> { take_while(|c| c != '\n')(i) } fn parse_str_variable_name(i: &str) -> IResult<&str, String> { let (i, id) = terminated( verify(anychar, |c| c.is_alphabetic()), tag($) )(i)?; let id = format!({}$, id); Ok((i, id)) } fn parse_int_variable_name(i: &str) -> IResult<&str, String> { map( preceded(not(digit1), alphanumeric1), String::from )(i) } fn parse_assignment(i: &str) -> IResult<&str, (String, Primitive)> { let (i, id) = alt(( parse_str_variable_name, parse_int_variable_name ))(i)?; let (i, _) = tag(=)(i)?; let (i, assigned_variable) = consume_line(i)?; Ok((i, (id.to_string(), Primitive::Assignment(assigned_variable.to_string())))) } ``` And that's worked (for what I want, anyway): `test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` But if I execute the simple program from the last post, and dump out the stored variables, I see: ```json {apple: Int(10), b$: String(Hello), cat: Assignment(apple)} ``` Which isn't quite right, because we should be assigning by value, not by reference (which is effectively what's happening there). So, if I amend the execution loop to add a specific case for Assignment: ```rust match item.1 { Command::Print(line) => println!({}, line), Command::GoTo(line) => iter.jump_to_line(line), Command::Var((id, Primitive::Assignment(variable))) => { self.vars.insert(id, self.vars.get(&variable).unwrap().clone()); } Command::Var((id, var)) => { self.vars.insert(id, var); } _ => panic!(Unrecognised command), } ``` So now instead when I encounter an Assignment, I lookup the actual value of the variable I'm assigning from, and inserting that as it's own value. Now, the output looks like: ```json {b$: String(Hello), apple: Int(10), cat: Int(10)} ``` ## Printing Okay, now I know how to read a variable, and assign a variable, I should now be able to print one out too. I'm going to add yet-another-enum, to represent a print output, which can either be a Value, or a Variable: ```rust #[derive(Debug, PartialEq, Eq, Clone)] pub enum PrintOutput { Value(String), Variable(String) } #[derive(Debug, PartialEq, Eq, Clone)] pub enum Command { Print(PrintOutput), GoTo(usize), Var((String, Primitive)), None, } ``` And then I have updated my parse for Print to read either a string, or a variable name: ```rust fn parse_print_command(i: &str) -> IResult<&str, PrintOutput> { alt(( map(alt(( parse_str_variable_name, parse_int_variable_name )), PrintOutput::Variable), map(read_string, PrintOutput::Value) ))(i) } let (i, cmd) = match command { PRINT => map(parse_print_command, Command::Print)(i)?, ... } ``` And then update the execution loop to use either of these new branches: ```rust match item.1 { Command::Print(PrintOutput::Value(line)) => println!({}, line), Command::Print(PrintOutput::Variable(variable)) => println!({:?}, self.vars.get(&variable).unwrap()), ... } ``` Now to test it with a new BASIC program: ```basic 10 LET a$=Hello 20 LET b$=World 30 PRINT a$ 40 PRINT b$ ``` ```command-line ╰─$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/basic-interpreter` String(Hello) String(World) ``` ## Quick addition: comments And quickly, just because it'll be relatively simple, I'm going to also parse comments, which in BASIC are marked as `REM`: ```rust fn match_command(i: &str) -> IResult<&str, &str> { alt((tag(PRINT), tag(GO TO), tag(LET), tag(REM)))(i) } fn parse_command(i: &str) -> IResult<&str, Command> { ... let (i, cmd) = match command { ... REM => { let (i, _) = consume_line(\n)(i)?; (i, Command::Comment) }, }; ... } ``` That's all I'll add to this part for now. But things are starting to come together! It won't be long before this can run the very-basic example program from [chapter 2 of the reference manual](https://worldofspectrum.org/ZXBasicManual/zxmanchap2.html): ```basic 10 REM temperature conversion 20 PRINT deg F, deg C 30 PRINT 40 INPUT Enter deg F, F 50 PRINT F,(F-32)*5/9 60 GO TO 40 ``` As always, the source code is on [Github](https://github.com/lewisdaleuk/basic-interpreter) (although it's in dire need of some cleanup).