LD

BASIC Interpreter Part 3: Copying values

In this post, I update my BASIC interpreter to allow me to reference other variables when creating new variables, as well as printing them and adding comments.

Now time for part three of my Sinclair BASIC Interpreter. In the previous post 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).

#[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:

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.

// 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:

{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:

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:

{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:

#[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:

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:

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:

10 LET a$=Hello
20 LET b$=World
30 PRINT a$
40 PRINT b$
╰─$ 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:

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:

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 (although it's in dire need of some cleanup).

Responses