Make A Language

Part Five: Binding Usages

5 October 2020

After Part Four, the longest so far, this will be a relatively short post: we’ll be adding support for binding usages. Here’s the syntax we’re after:

let a = 10
let b = a

where a is a binding usage.

Parsing

Let’s begin with the parser. Add pub mod binding_usage; to lib.rs, and create a new file at src/binding_usage.rs. As usual, we’ll start with a test:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_binding_usage() {
        assert_eq!(
            BindingUsage::new("abc"),
            Ok((
                "",
                BindingUsage {
                    name: "abc".to_string(),
                },
            )),
        );
    }
}

Let’s add the definition of BindingUsage:

#[derive(Debug, PartialEq)]
pub struct BindingUsage {
    name: String,
}

Since the syntax we’re using for binding usages is to simply write out the name of the binding being used,1 all BindingDef::new has to do is call utils::extract_ident:

use crate::utils;

// snip

impl BindingUsage {
    pub fn new(s: &str) -> Result<(&str, Self), String> {
        let (s, name) = utils::extract_ident(s)?;

        Ok((
            s,
            Self {
                name: name.to_string(),
            },
        ))
    }
}

Let’s check if the parser is working as expected:

$ cargo t -q
running 25 tests
.........................
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Now that the parser is working, we can move on to evaluating these binding usages.

Evaluation

It’s been a while since we covered evaluation, so here’s how it works: binding names and their values are stored in the evaluation environment, or Env. Binding definitions add values to the Env, while binding usages reference these.

First, a test:

use crate::env::Env;
use crate::utils;
use crate::val::Val;

// snip

#[cfg(test)]
mod tests {
    // snip

    #[test]
    fn eval_existing_binding_usage() {
        let mut env = Env::default();
        env.store_binding("foo".to_string(), Val::Number(10));

        assert_eq!(
            BindingUsage {
                name: "foo".to_string(),
            }
            .eval(&env),
            Ok(Val::Number(10)),
        );
    }
}

We create an empty environment, and then declare a binding with the name foo and a value of 10. After this, we try evaluating a binding usage of the binding with the name foo, and assert that it gives us 10. Notice that the output of .eval(&env) is a Result – this is because we want to return an appropriate error if the binding does not exist.

Let’s write the eval method:

impl BindingUsage {
    // snip

    pub(crate) fn eval(&self, env: &Env) -> Result<Val, String> {
        env.get_binding_value(&self.name)
    }
}

This calls a hypothetical Env::get_binding_value method, which we can write now:

// env.rs

impl Env {
    // snip

    pub(crate) fn get_binding_value(&self, name: &str) -> Result<Val, String> {
        self.bindings
            .get(name)
            .cloned()
            .ok_or_else(|| format!("binding with name ‘{}’ does not exist", name))
    }
}

We need to use .cloned() because HashMap::get returns a reference to the value, while we want ownership. Keep in mind that using strings for error messages, as we have been doing throughout this series, is unideal – we’re doing it only because it makes our lives easier for now. Eventually we’ll restructure the project to use custom error enums with Display implementations, but that’s for another day.

This code doesn’t compile because Val doesn’t implement Clone, so let’s fix that:

// val.rs

#[derive(Debug, Clone, PartialEq)]
pub enum Val {
    Number(i32),
}

We should add a test to verify that the error message for trying to evaluate a non-existent binding is correct:

// binding_usage.rs

#[cfg(test)]
mod tests {
    // snip

    #[test]
    fn eval_non_existent_binding_usage() {
        let empty_env = Env::default();

        assert_eq!(
            BindingUsage {
                name: "i_dont_exist".to_string(),
            }
            .eval(&empty_env),
            Err("binding with name ‘i_dont_exist’ does not exist".to_string()),
        );
    }
}

And with that, we’re done:

$ cargo t -q
running 27 tests
...........................
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

In the next instalment of the series, we’ll add support for blocks.


  1. This may seem painfully obvious if you haven’t used languages where this isn’t the case. In actuality, though, there are languages where accessing the value of a binding isn’t as simple as writing out the binding’s name: Bash, for example, insists2 you prefix the binding’s name with $. ↩︎

  2. Yes, I know that the $ is needed because Bash has bare words (how else would you tell if something is a usage of a binding or a bare word?), but who doesn’t like making fun of Bash’s syntax? (seriously though, I can never remember the difference (or lack thereof) between if test condition, if [ condition ] and if [[ condition ]]) ↩︎