rocks_lang/
lib.rs

1#![allow(clippy::needless_return)]
2
3//! Rocks is a programming language written in Rust. It is a dynamically typed language with
4//! lexical scoping and first-class functions. Rocks is a tree-walk interpreter with a hand-written
5//! recursive descent parser. Rocks is a hobby project and is not intended for production use.
6//!
7//! Rocks is a dynamically typed language. This means that the type of a variable is determined at
8//! runtime. This is in contrast to statically typed languages, where the type of a variable is
9//! determined at compile time. Dynamically typed languages are often easier to use but are
10//! generally slower than statically typed languages.
11//!
12//! Rocks is a tree-walk interpreter. This means that the interpreter walks the abstract syntax tree
13//! (AST) and evaluates each node. This is in contrast to a compiler, which would convert the AST
14//! into bytecode or machine code. Tree-walk interpreters are generally easier to implement than
15//! compilers, but are generally slower than compilers.
16//!
17//! Rocks is a hobby project and is not intended for production use. The goal of this project is to
18//! learn more about programming languages and interpreters. This project is inspired by the
19//! [Crafting Interpreters](https://craftinginterpreters.com/) book by Bob Nystrom.
20//!
21//! ## Scanning
22//! The first step in the interpreter is scanning. Scanning is the process of converting a string of
23//! characters into a list of tokens. A token is a single unit of a programming language. For
24//! example, the string `1 + 2` would be converted into the following tokens:
25//! ```text
26//! [Number(1), Plus, Number(2)]
27//! ```
28//! The scanner is implemented in the [`scanner`](scanner) module as an iterator over the characters
29//! in the source code. It is a simple state machine that returns the next token in the source code
30//! when called.
31//!
32//! The scanner reports syntax errors in the source code as a [`ScanError`](error::ScanError).
33//! These errors are trivial problems like an unterminated string literal or an unexpected character.
34//! Scan errors are reported as soon as they are encountered. This means that the scanner will
35//! continue scanning the source code even if it has already encountered a syntax error. This is
36//! useful because it allows the user to fix multiple syntax errors at once.
37//!
38//! ## Parsing
39//! The second step in the interpreter is parsing. Parsing is the process of converting a list of
40//! tokens into an abstract syntax tree (AST). The parser is implemented in the [`parser`](parser)
41//! module as a recursive descent parser. The parser transforms the list of tokens into expressions
42//! and statements. [`Expressions`](expr::Expr) are pieces of code that produce a value, specifically an
43//! [`Object`](object::Object). Objects are an umbrella term for all types of values in Rocks
44//! including literals, functions, classes and instances. [`Statements`](stmt::Stmt) are pieces of code
45//! that do not produce a value but instead, perform some action. These actions modify the state of the
46//! program and thus, are called side-effects. For example, a variable declaration or an if clause
47//! would be classified as statements.
48//!
49//! For example, the string `print 1 + 2;` would be converted into the following AST:
50//! ```text
51//! PrintStatement {
52//!     BinaryExpression {
53//!         left: Number(1),
54//!         operator: Plus,
55//!         right: Number(2),
56//!     }
57//! }
58//! ```
59//! The parser reports syntax errors in the source code as a [`ParseError`](error::ParseError).
60//! Unlike the scanner, the parser catches errors that span multiple tokens. For example, the
61//! following expression is invalid because it is missing the right-hand operand:
62//! ```text
63//! 1 !=
64//! ```
65//! However, much like the scanner, the parser will continue parsing the source code even if it
66//! has already encountered a syntax error using a technique called synchronization. This is useful
67//! because it allows the user to fix multiple syntax errors at once.
68//!
69//! ## Resolving
70//! The third step in the interpreter is resolving. Resolving is the process of statically analyzing
71//! the AST to determine the scope of each variable. While this requires a pre-pass of the AST, it
72//! is necessary to construct robust lexical scoping. The resolver is implemented in the
73//! [`resolver`](resolver) module as a tree-walk interpreter. The resolver is run after the parser
74//! because it requires the AST to be fully constructed. The resolver reports errors as a
75//! [`ResolveError`](error::ResolveError). These errors are syntactically valid but semantically invalid.
76//! and therefore, cannot be caught by the scanner or the parser. For example, the following expression
77//! is valid a valid Rocks syntax but it is semantically invalid because the variable `a` is defined
78//! twice in the same scope:
79//! ```text
80//! {
81//!    var a = 1;
82//!    var a = 2;
83//! }
84//! ```
85//!
86//! ## Interpreting
87//! The final step in the interpreter is _interpreting_. Interpreting is the process of evaluating the
88//! AST. The interpreter is implemented in the [`interpreter`](interpreter) module as a tree-walk
89//! interpreter. Thanks to all the previous steps, the interpreter is able to evaluate the AST and produce
90//! a result. The interpreter reports errors as a [`RuntimeError`](error::RuntimeError). While the
91//! scanner, the parser and the resolver try to catch as many errors as possible before running the
92//! code, most errors can only be caught at runtime. For example, the following expression is valid
93//! Rocks syntax but it is semantically invalid because it tries to add a string and a number:
94//! ```text
95//! var a = "123";
96//! var b = a + 123;
97//! ```
98//! The interpreter is also responsible for managing the environment. The environment is a mapping of
99//! variable names to their values. The environment is implemented in the [`environment`](environment)
100//! module as a stack of hash maps. Each hash map represents a scope in the program. This allows the
101//! interpreter to implement lexical scoping. The interpreter also manages the call stack.
102
103use std::{fs, process};
104
105pub mod error;
106pub mod token;
107pub mod scanner;
108pub mod expr;
109pub mod stmt;
110pub mod environment;
111pub mod parser;
112pub mod ast;
113pub mod interpreter;
114pub mod literal;
115pub mod object;
116pub mod function;
117pub mod resolver;
118pub mod class;
119
120use parser::Parser;
121use scanner::Scanner;
122use resolver::Resolver;
123
124#[allow(non_camel_case_types)]
125pub struct rocks<'w> {
126    interpreter: interpreter::Interpreter<'w>,
127}
128
129impl<'w> rocks<'w> {
130    pub fn new<W: std::io::Write>(writer: &'w mut W) -> Self {
131        rocks {
132            interpreter: interpreter::Interpreter::new(writer),
133        }
134    }
135
136    pub fn run_file(&mut self, path: String) {
137        let contents = fs::read_to_string(path)
138            .expect("Should have been able to read the file");
139
140        self.run(contents);
141
142        if error::did_error() {
143            process::exit(65);
144        }
145    }
146
147    pub fn run_prompt(&mut self) {
148        let mut rl = rustyline::DefaultEditor::new().unwrap();
149
150        let history_path = home::home_dir().unwrap().join(".rocks.history");
151        rl.load_history(&history_path).ok();
152
153        loop {
154            let readline = rl.readline("> ");
155            match readline {
156                Ok(mut line) => {
157                    rl.add_history_entry(line.as_str()).unwrap();
158                    line.push('\n');
159                    self.run(line);
160                    error::reset_error();
161                },
162                Err(_) => {
163                    break;
164                }
165            }
166        }
167
168        rl.save_history(&history_path).ok();
169    }
170
171    fn run(&mut self, mut source: String) {
172        if !source.ends_with('\n') {
173            source.push('\n');
174        }
175
176        let mut scanner = Scanner::new(&source);
177        let tokens = scanner.scan_tokens();
178
179        if error::did_error() {
180            return;
181        }
182
183        let mut parser = Parser::new(tokens);
184        let statements = parser.parse();
185
186        if error::did_error() {
187            return;
188        }
189
190        let mut resolver = Resolver::new(&mut self.interpreter);
191        resolver.resolve(&statements);
192
193        if error::did_error() {
194            return;
195        }
196
197        self.interpreter.interpret(&statements);
198    }
199}
200
201impl<'w> Default for rocks<'w> {
202    fn default() -> Self {
203        Self::new(Box::leak(Box::new(std::io::stdout())))
204    }
205}