LD
Change your colour scheme

BASIC Interpreter Part 3: Copying values

Published:

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).

About the author

My face

I'm Lewis Dale, a software engineer and web developer based in the UK. I write about writing software, silly projects, and cycling. A lot of cycling. Too much, maybe.

Responses