BASIC Interpreter Part 3: Copying values
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).