Building an interpreter: Representing values.
Before looking at our interpreter’s runtime environment (or namespace), we need an object system that supports the objects we manipulate within the runtime environment. This short text introduces that scaffold for our object system.
Consider the snippet below:
let v = "hello world";
let pi = 3.14;The values above represent two different types in our language with different sets of methods and potentially different methods of storage and representation in memory, but these must be supported by our language. We thus must answer these questions in order to choose our representation of value -
How do we represent the different types that our language supports? and
How do we store these values in our language, given their differences?
Type representation
We want to keep our systems for representing types as simple as they can be for now. To do this, we design our object systems so that we deal with values generically till the very moment when we need to be specific. This means that in our snippet above, we only want to deal with the fact that we are manipulating a string or float type when we have to perform an operation unique to each of those types. Another way to think of this is that we want all built-in types to have a singular parent type.
The simplest options we have in Rust for implementing this idea are:
Using an enum of types to represent the various types.
Representing the various types as trait objects.
Each option here has it pros and cons. Option #1 is good enough when we know all possible types that our language can support ahead of time and this is where our little language falls into at this point1. We call the enum of our built-in types, SoxObject.
We model our SoxObject as the enum shown below
use std::fmt::Debug;
use crate::int::SoxIntRef;
use crate::string::SoxStringRef;
#[macro_export]
macro_rules! payload {
($e:expr, $p:path) => {
match $e {
$p(v) => Some(v),
_ => None
}
};
}
#[derive(Clone, Debug)]
pub enum SoxObj {
Int(SoxIntRef),
String(SoxStringRef),
}
The enum currently has just the integer and string variants but this will expand as we progress. To extract the value that the enum references, we define a macro, payload that is used as shown below.
fn main(){
let v = SoxInt::new(10).into_sox_obj();
let val = payload!(v, SoxObj::Int).unwrap();
println!("value is {:?}", val);
}We use a macro here because it is the simplest means of implementing that functionality. Recall enum variants in Rust are not types so you cannot pass them into your regular functions or methods so we cannot just define a simplke function or method that implements the functionality we need here.
Our implementation of the String variant shown in our definition above is as follows.
use std::rc::Rc;
use crate::core::SoxObj;
pub type SoxStringRef = Rc<SoxString>;
#[derive(Debug)]
pub struct SoxString {
value: String,
}
impl SoxString {
pub fn new(val: String) -> Self {
SoxString {
value: val,
}
}
pub fn into_ref(self) -> SoxStringRef {
return Rc::new(self);
}
pub fn into_sox_obj(self) -> SoxObj {
return SoxObj::String(self.into_ref());
}
}The Integer variant follows the same pattern as shown below.
use std::rc::Rc;
use crate::core::SoxObj;
pub type SoxIntRef = Rc<SoxInt>;
#[derive(Debug)]
pub struct SoxInt {
value: i64,
}
impl SoxInt {
pub fn new(val: i64) -> Self {
SoxInt {
value: val,
}
}
pub fn into_ref(self) -> SoxIntRef {
return Rc::new(self);
}
pub fn into_sox_obj(self) -> SoxObj {
return SoxObj::Int(self.into_ref());
}
}
Using references to objects
In our SoxObject definition, we have used types like SoxIntRef and SoxStringRef. These are defined as reference counted versions of SoxInt and SoxString. This is due to the ownership model of Rust that ensures that a value can have only one owner unlike our own language Sox where multiple variables can mutably reference a values.
Storing values
So far, we have deferred to Rust to decide where and how our objects are stored. Our SoxInt type is a simple struct wrapper around an i64 and this goes on the stack. Our SoxString type is anther wrapper around the String type. This type goes on the stack but points to a value on the heap, again this is Rust implementation
That is all there is to implementing our objects for now. As we progress, we will expand and make significant changes to our object system.
There is a performance cost to using enums justr as there is in using trait objects. In Rust, the size of an enum is the sum of the size of the largest variant and the tag that enums use to identify the type the enum holds but we do bot pay attention to this for now.

