lewisdale.dev/src/blog/posts/2023/1/basic-interpreter-part-3-copying-values.md
2023-12-26 14:35:09 +00:00

7.2 KiB

title date slug
BASIC Interpreter Part 3: Copying values 2023-01-20T16:13:56 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).