Document not found (404)
This URL is invalid, sorry. Please use the navigation bar or search to continue.
Repository: vishpat/lisp-rs Branch: main Commit: cd8bb3b0f75a Files: 39 Total size: 778.0 KB Directory structure: gitextract_y9aowojs/ ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── docs/ │ ├── .nojekyll │ ├── 404.html │ ├── FontAwesome/ │ │ └── css/ │ │ └── font-awesome.css │ ├── ayu-highlight.css │ ├── book.js │ ├── css/ │ │ ├── chrome.css │ │ ├── general.css │ │ ├── print.css │ │ └── variables.css │ ├── evaluator.html │ ├── fonts/ │ │ ├── OPEN-SANS-LICENSE.txt │ │ ├── SOURCE-CODE-PRO-LICENSE.txt │ │ └── fonts.css │ ├── highlight.css │ ├── highlight.js │ ├── index.html │ ├── introduction.html │ ├── lexer.html │ ├── next.html │ ├── overview.html │ ├── parser.html │ ├── print.html │ ├── repl.html │ ├── searcher.js │ ├── searchindex.js │ ├── searchindex.json │ └── tomorrow-night.css └── src/ ├── env.rs ├── eval.rs ├── lexer.rs ├── lib.rs ├── main.rs ├── object.rs └── parser.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target ================================================ FILE: .rustfmt.toml ================================================ max_width=60 tab_spaces=2 ================================================ FILE: Cargo.toml ================================================ [package] name = "lisp-rs" version = "0.3.5" edition = "2021" license = "MIT" homepage = "https://vishpat.github.io/lisp-rs" repository = "https://github.com/vishpat/lisp-rs" documentation = "https://vishpat.github.io/lisp-rs" description = """ Lisp interpreter in Rust """ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] linefeed = {version = "0.6.0", optional = true } [features] build-binary = ["linefeed"] [lib] name = "lisp_rs" path = "src/lib.rs" [[bin]] name = "lisp-rs" path = "src/main.rs" required-features = ["build-binary"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Vishal Patil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # lisp-rs A simple Lisp interpreter/library in Rust. The interpreter/library was initially developed as a **teaching aid** to explain how [Lisp interpreters work](https://www.amazon.com/dp/B0DS8XJJWY/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=&sr=) and how can they be implemented using the Rust programming language. However the interpreter seems to be taking life of it's own and has been ported to the [web](https://vishpat.github.io/lisp-rs-wasm) using [WASM](https://webassembly.org). The interpreter is available as a [crate](https://crates.io/crates/lisp-rs) and can be used to embed a Lisp interpreter in your Rust projects. The WASM implementation uses the lisp-rs as a library to implement the online interpreter. ## Dialect The interpreter is based on a modified subset of [Scheme](https://en.wikipedia.org/wiki/Scheme_(programming_language)). Following are the features supported by the interpreter - Variables and Constants - Functions (lambdas) - Functional constructs such as map, filter and reduce - Closures - Tail Call Optimization More information about the dialect can be found at - [Syntax](https://github.com/vishpat/lisp-rs/wiki/Lisp-Syntax) - [Sample Programs](https://github.com/vishpat/lisp-rs/wiki/Sample-programs) ## Implementation For a detailed code-walkthrough about evaluator refer to the [docs](https://vishpat.github.io/lisp-rs). For code-walkthrough of all the phases, get the [book]( https://www.amazon.com/dp/B0DS8XJJWY/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=&sr=). [](https://asciinema.org/a/VVQQfGpp15a4BaoNgnEKIqqrr) ## REPL ``` cargo run --features="build-binary" ``` ## Test ``` cargo test ``` ## WASM The interpreter has also been compiled to WASM so that it can run in a browser and is hosted [here](https://vishpat.github.io/lisp-rs-wasm). ================================================ FILE: docs/.nojekyll ================================================ This file makes sure that Github Pages doesn't process mdBook's output. ================================================ FILE: docs/404.html ================================================
This URL is invalid, sorry. Please use the navigation bar or search to continue.
Now comes the most exciting part of the project. Evaluation is the final step that will produce the result for the Lisp program. At a high level, the evaluator function recursively walks the List-based structure created by the parser and evaluates each atomic object and list (recursively), combines these intermediate values, and produces the final result.
The evaluator is implemented using the recursive eval_obj function. The eval_obj function takes the List object representing the program and the global env variable (a simple hashmap) as the input. The function then starts processing the List object representing the program by iterating over each element of this list
fn eval_obj(obj: &Object, env: &mut Rc<RefCell<Env>>)
-> Result<Object, String>
{
match obj {
Object::Void => Ok(Object::Void),
Object::Lambda(_params, _body) => Ok(Object::Void),
Object::Bool(_) => Ok(obj.clone()),
Object::Integer(n) => Ok(Object::Integer(*n)),
Object::Symbol(s) => eval_symbol(s, env),
Object::List(list) => eval_list(list, env),
}
}
In the case of the atomic objects such as an integer and boolean, the evaluator simply returns a copy of the object. In the case of the Void and Lambda (function objects), it returns the Void object. We will now walk through the eval_symbol and eval_list functions which implement most of the functionality of the evaluator.
Before understanding the eval_symbol function, it is important to understand the design of how variables are implemented for the Lisp interpreter.
The variables are just string labels assigned to values and they are created using the define keyword. Note a variable can be assigned atomic values such as integer or a boolean or it can be assigned function objects
(
(define x 1)
(define sqr (lambda (r) (* r r)))
)
The above Lisp code defines (or creates) two variables with the names x and sqr that represent an integer and function object respectively. Also, the scope of these variables lies within the list object that they are defined under. This is achieved by storing the mapping from the variable names (strings) to the objects in a map-like data structure called Env as shown below.
// env.rs
pub struct Env {
parent: Option<Rc<RefCell<Env>>>,
vars: HashMap<String, Object>,
}
The interpreter creates an instance of Env at the start of the program to store the global variable definitions. In addition, for every function call, the interpreter creates a new instance of env and uses the new instance to evaluate the function call. This new instance of env contains the function parameters as well as a back pointer to the parent env instance from where the function is called as shown below with an example
(
(define m 10)
(define n 12)
(define K 100)
(define func1 (lambda (x) (+ x K)))
(define func2 (lambda (x) (- x K)))
(func1 m)
(func2 n)
)

This concept will become clearer as we will walk through the code.
The job of eval_symbol is to look up the Object bound to the symbol. This is done by recursively looking up in the passed env variable or any of its parent env until the root env of the program.
let val = env.borrow_mut().get(s);
if val.is_none() {
return Err(format!("Unbound symbol: {}", s));
}
Ok(val.unwrap().clone())
The eval_list function is the core of the evaluator and is implemented as shown below.
let head = &list[0];
match head {
Object::Symbol(s) => match s.as_str() {
"+" | "-" | "*" | "/" | "<" | ">" | "=" | "!=" => {
return eval_binary_op(&list, env);
}
"if" => eval_if(&list, env),
"define" => eval_define(&list, env),
"lambda" => eval_function_definition(&list),
_ => eval_function_call(&s, &list, env),
},
_ => {
let mut new_list = Vec::new();
for obj in list {
let result = eval_obj(obj, env)?;
match result {
Object::Void => {}
_ => new_list.push(result),
}
}
Ok(Object::List(new_list))
}
}
This function peeks at the head of the list and if the head does not match the symbol object, it iterates all of the elements of the list recursively evaluating each element and returning a new list with the evaluated object values.
If the head of the list in the eval_list function matches the define keyword, for example
(define sqr (lambda (x) (* x x)))
the eval_define function calls eval_obj on the third argument of the list and assigns the evaluated object value to the symbol defined by the second argument in the list. The symbol and its object value are then stored in the current env.
let sym = match &list[1] {
Object::Symbol(s) => s.clone(),
_ => return Err(format!("Invalid define")),
};
let val = eval_obj(&list[2], env)?;
env.borrow_mut().set(&sym, val);
In the example above the symbol sqr and the function object representing the lambda will be stored in the current env. Once the function sqr has been defined in this manner, any latter code can access the corresponding function object by looking up the symbol sqr in env.
If the head of the list in the eval_list function matches a binary operator, the list is evaluated on the basis of the type of the binary operator, for example
(+ x y)
the eval_binary_op function calls the eval_obj on the second and third element of the list and performs the binary sum operation on the evaluated values.
If the head of the list in the eval_list function matches the if keyword, for example
(if (> x y) x y)
the eval_if function calls eval_obj on the second element of the list and depending upon whether the evaluated value is true or false, calls the eval_obj on either the third or fourth element of the list and returns the value
let cond_obj = eval_obj(&list[1], env)?;
let cond = match cond_obj {
Object::Bool(b) => b,
_ => return Err(format!("Condition must be a boolean")),
};
if cond == true {
return eval_obj(&list[2], env);
} else {
return eval_obj(&list[3], env);
}
As mentioned earlier, the lambda (or function) object consists of two vectors
Lambda(Vec<String>, Vec<Object>)
If the head of the list in the eval_list function matches the lambda keyword, for example
(lambda (x) (* x x))
the eval_function_definition function evaluates the second element of the list as a vector of parameter names.
let params = match &list[1] {
Object::List(list) => {
let mut params = Vec::new();
for param in list {
match param {
Object::Symbol(s) => params.push(s.clone()),
_ => return Err(format!("Invalid lambda parameter")),
}
}
params
}
_ => return Err(format!("Invalid lambda")),
};
The third element of the list is simply cloned as the function body.
let body = match &list[2] {
Object::List(list) => list.clone(),
_ => return Err(format!("Invalid lambda")),
};
Ok(Object::Lambda(params, body))
The evaluated parameter and body vector are returned as the lambda object
If the head of the list is a Symbol object and it does not match any of the aforementioned keywords or binary operators, the interpreter assumes that the Symbol object maps to a Lambda (function object). An example of the function call in Lisp is as follows
(find_max a b c)
To evaluate this list the eval_function_call function is called. This function first performs the lookup for the function object using the function name, find_max in the case of this example.
let lamdba = env.borrow_mut().get(s);
if lamdba.is_none() {
return Err(format!("Unbound symbol: {}", s));
}
If the function object is found, a new env object is created. This new env object has a pointer to the parent env object. This is required to get the values of the variables not defined in the scope of the function.
let mut new_env = Rc::new(
RefCell::new(
Env::extend(env.clone())));
The next step in evaluating the function call requires preparing the function parameters. This is done by iterating over the remainder of the list and evaluating each parameter. The parameter name and the evaluated object are then set in the new env object.
for (i, param) in params.iter().enumerate() {
let val = eval_obj(&list[i + 1], env)?;
new_env.borrow_mut().set(param, val);
}
Finally, the function body is evaluated by passing the new_env, which contains the parameters to the function
let new_body = body.clone();
return eval_obj(&Object::List(new_body), &mut new_env);
The lisp-rs project implements an interpreter, in Rust, for a small subset of Scheme, a Lisp dialect. The main goal of this document is to make sure the reader understands the inner details of how the interpreter was implemented.
The project was inspired by Peter Norvig's article (How to Write a (Lisp) Interpreter (in Python)) and the book Writing An Interpreter In Go. This document serves as a commentary on the code that implements the interpreter. Rust's rich programming constructs such as enum, pattern matching, and error handling make it easy and a joy to implement this bare-bone interpreter.
To make the most out of this project, it is expected that the user is aware of the following Computer Science concepts
Rust is a non-trivial language, however, to implement the Lisp interpreter, the reader needs to have moderate experience with the language. Knowing the following four concepts should be enough for the user to understand the whole project
In order to keep the interpreter simple and its implementation easy to understand, the number of features supported by it has been limited on purpose. Following are the data types and statements that will be supported by the interpreter.
Following are some of the sample programs that you will be able run using the interpreter
(
(define factorial (lambda (n) (if (< n 1) 1 (* n (factorial (- n 1))))))
(factorial 5)
)
(
(define pix 314)
(define r 10)
(define sqr (lambda (r) (* r r)))
(define area (lambda (r) (* pix (sqr r))))
(area r)
)
The interpreter will be implemented from scratch and without the help of any tools such as nom or pest. The interpreter implementation is broken down into four parts
The best way to understand the implementation of the interpreter is to check out the code and walk through it while reading this document.
git clone https://github.com/vishpat/lisp-rs
git checkout 0.0.1
Once you thoroughly understand the implementation, you will be equipped to add new features to it, such as support for new data types like strings, floating-point numbers, lists, or functional programming constructs such as map, filter, reduce functions, etc.
To run the interpreter and get its REPL (Read-Eval-Print-Loop)
cargo run