You might be surprised. There's plenty of information out there on how to build interpreters, and Rust is a really nice language to build one in, especially for a first-timer.
If nothing else, it's a great learning exercise that mostly doesn't need terribly much background (there's a lot of old theory about parser generators that it turns out isn't actually terribly helpful for real languages past maybe tokenization)
The top level work for a full (if inefficient) version is:
- lex a source string into a sequence of tokens
- parsing a sequence of tokens into an AST
- interpreting by walking the AST
- implement the standard library
- wrapping in standard developer tooling like REPLs and package management
The most confusing is probably the first: lexing requires a lot of care that you're not off by one matching spaces, etc., so use lots of unit tests! The second is just matching and looping and peeking at the next token in a way that's pretty much a direct match to the grammar so it turns out to be very obvious most of the time.
Both of these should use an interface that looks essentially like: fn parse_foo(input: &str) -> Result<(Foo, &str), Error>
(where lexing is just parsing a token), where you return the parsed value and the rest of the unparsed input or the parse error, a pattern that lets you easily compose parsers by just calling them in sequence with the previous results or bubble the parse error out with ?
.
There's a newish approach called "Parsing Expression Grammars" (PEGs) which are a lot more succinct, but I wouldn't try that my first time out, it's really easy to misunderstand what you're actually writing and can be really hard to exactly match existing languages.
Interpreting is mostly incredibly boring, easy code for most languages: fn eval_foo(foo: &Foo, context: &mut Context) -> Result<Value, Error>
where Context
can have whatever you like but generally will have a chain of namespaces in scope (which are really just a map from name to value). It mostly looks like repeating the matched syntax tree with the equivalent Rust code after unwrapping all the error cases, so this is an exercise in not getting too distracted by giving better errors.
Implementing a standard library is the bulk of getting real code to run. See if you can get a full API list and generate a bunch of stubs, to get things running as quick as possible. You can even do things like resolve any unknown function calls in your evaluator to a "stub" value that names the unimplemented API but otherwise is ignored by everything, to get code just chugging straight through no matter how much is missing. This is really effective at keeping momentum up!