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}