1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
use crate::basic_io::BasicIO;
use crate::chunk::Chunk;
use crate::compiler::Compiler;
use crate::error_handler::ErrorHandler;
use crate::expr::ExprValue;
use crate::function::{Function, FunctionDefinition, FunctionType};
use crate::native_function::{NativeFunction, NativeFunctionDefinition};
use crate::opcodes::OpCode;
use crate::parser::Parser;
use crate::resolver::Resolver;
use crate::scanner::Scanner;
use crate::std::Std;
use crate::tokens::TokenType;
use crate::vm::VirtualMachine;
use std::collections::HashMap;

/// Used for building a custom `XBasic` interpreter
/// There are two main reasons to use this:
/// 1. To define custom native functions
/// 2. To set a compute limit
pub struct XBasicBuilder<T: 'static> {
	stdio: T,
	native_functions: HashMap<String, NativeFunction<T>>,
	compute_limit: usize,
}

impl<T> XBasicBuilder<T>
where
	T: BasicIO,
{
	/// Creates a new `XBasicBuilder` struct with the given `BasicIO` instance.
	///
	/// # Arguments
	///
	/// * `stdio` - An instance of a struct which implements the `BasicIO` trait.
	pub fn new(stdio: T) -> Self {
		let mut xbb = Self {
			stdio,
			native_functions: HashMap::new(),
			compute_limit: 0, // No limit
		};
		Std::attach(&mut xbb);
		xbb
	}

	/// Defines a new native function.
	/// Returns `Err` if the function name is already in use by a native function, or if the native function's arity is more than 255. Returns `Ok` otherwise.
	///
	/// # Arguments
	///
	/// * `name` - The name of the native function.
	/// * `arity` - The arity of the function, i.e. the number of arguments it expects.
	/// * `function` - The closure that implements the function. This is passed a vec of arguments, as well as a mutable reference to the BasicIO object. It is ensured that `arguments.len() == arity`.
	pub fn define_function<F: 'static>(
		&mut self,
		name: String,
		arity: u8,
		function: F,
	) -> Result<(), ()>
	where
		F: Fn(Vec<ExprValue>, &mut T) -> ExprValue,
	{
		if self.native_functions.len() < 256 {
			// Prevent defining the same native function twice
			if self.native_functions.contains_key(&name) {
				return Err(());
			}

			self.native_functions
				.insert(name.clone(), NativeFunction::new(name, arity, function));
			Ok(())
		} else {
			Err(())
		}
	}

	/// Set the compute limit, in terms of number of instructions.
	/// Set to 0 for no limit (the default)
	pub fn compute_limit(&mut self, limit: usize) {
		self.compute_limit = limit;
	}

	/// Consume the builder, producing an XBasic instance.
	pub fn build(self) -> XBasic<T> {
		let native_function_definitions = self
			.native_functions
			.iter()
			.map(|(_, v)| NativeFunctionDefinition::new(v.name.clone(), v.arity))
			.collect();

		XBasic {
			error_handler: ErrorHandler::new(),
			functions: Vec::new(),
			native_functions: native_function_definitions,
			compiler: Compiler::new(),
			vm: VirtualMachine::new(
				self.stdio,
				self.native_functions.into_iter().map(|(_, v)| v).collect(),
				self.compute_limit,
			),
		}
	}
}

/// Represents an xBASIC Interpreter.
pub struct XBasic<T: 'static> {
	/// Keeps track of errors that have been encountered while interpreting source code.
	/// Errors are recorded as user-friendly strings so that they can be passed directly to the user.
	pub error_handler: ErrorHandler,
	functions: Vec<Function>,
	native_functions: Vec<NativeFunctionDefinition>,
	compiler: Compiler,
	vm: VirtualMachine<T>,
}

impl<T> XBasic<T>
where
	T: BasicIO,
{
	/// Creates a new `XBasic` struct with the given `BasicIO` instance.
	pub fn new(stdio: T) -> Self {
		let xbb = XBasicBuilder::new(stdio);
		xbb.build()
	}

	/// Runs a snippet of XBasic source code using the interpreter. Returns `Ok` if there are no errors. Otherwise returns `Err` and keeps track of errors in the `error_handler` field.
	///
	/// # Arguments
	///
	/// * source - The XBasic code. This should be terminated by a newline character.
	pub fn run(&mut self, source: &str) -> Result<(), ()> {
		let mut sc = Scanner::new(source, &mut self.error_handler);
		let tokens = sc.scan_tokens();

		let tokens = match tokens {
			Ok(x) => x,
			Err(_) => return Err(()),
		};

		// If a program consists of only newlines and an Eof, we shouldn't run it
		let mut only_whitespace = true;
		for token in &tokens {
			if token.token_type != TokenType::Newline && token.token_type != TokenType::Eof {
				only_whitespace = false;
				break;
			}
		}
		if only_whitespace {
			return Ok(());
		}

		let mut function_definitions = {
			let mut resolver = Resolver::new(tokens.clone(), &mut self.error_handler);
			match resolver.resolve(
				&self
					.functions
					.iter()
					.map(|a| a.definition())
					.collect::<Vec<_>>(),
			) {
				Some(x) => x,
				None => return Err(()),
			}
		};

		// Tack on native functions
		// O(n^2) but should still be fast enough
		for function in &self.native_functions {
			let mut unique = true;
			for other in &function_definitions {
				if function.name == other.name {
					unique = false;
					break;
				}
			}
			if unique {
				function_definitions.push(FunctionDefinition::new(
					function.name.to_string(),
					function.arity,
					0,
					FunctionType::Native,
				));
			}
		}

		let mut parser = Parser::new(tokens, function_definitions, &mut self.error_handler);
		let stmts = match parser.parse() {
			Some((stmts, functions)) => {
				// Combine new functions with functions that we already know about
				for func in functions {
					self.functions.push(func);
				}
				stmts
			}
			None => return Err(()),
		};

		let native_function_ids: HashMap<String, usize> = self
			.native_functions
			.iter()
			.enumerate()
			.map(|(i, f)| (f.name.clone(), i))
			.collect();

		match self.compiler.compile(
			stmts,
			&mut self.functions,
			native_function_ids,
			&mut self.error_handler,
		) {
			Some(chunk) => {
				self.vm.run(
					chunk,
					self.functions
						.iter()
						.cloned()
						.map(|func| func.chunk.unwrap())
						.collect(),
					&mut self.error_handler,
				);
			}
			None => return Err(()),
		}

		if self.error_handler.had_runtime_error {
			return Err(());
		}

		Ok(())
	}

	/// Clears all existing errors in the various stages of the interpreter.
	pub fn clear_errors(&mut self) {
		self.error_handler.reset();
		self.compiler.clear_errors();
		self.vm.clear_errors();
	}

	/// Calls an xBASIC function from Rust code.
	/// # Arguments
	///
	/// * name - The name of the function to call
	/// * arguments - The arguments of the function to be called
	pub fn call_function(&mut self, name: &str, arguments: &[ExprValue]) -> Result<ExprValue, ()> {
		for (i, function) in self.functions.iter().enumerate() {
			if function.name == name {
				if function.parameters.len() != arguments.len() {
					// Arity mismatch
					return Err(());
				}
				// Generate instructions for function call
				let mut instructions = Vec::new();

				// Add arguments as literals
				for i in 0..arguments.len() {
					instructions.push(OpCode::Literal8 as u8);
					instructions.push(i as u8);
				}

				// Add call opcode
				instructions.push(OpCode::Call as u8);
				instructions.push(i as u8);

				// Generate line numbers(all set to 0, since they should never be used)
				let lines = instructions.iter().map(|_| 0).collect();
				let chunk = Chunk::new(instructions, lines, arguments.to_vec(), 0, 0);

				// Run the instructions
				if !self.vm.run(
					chunk,
					self.functions
						.iter()
						.cloned()
						.map(|func| func.chunk.unwrap())
						.collect(),
					&mut self.error_handler,
				) {
					return Err(());
				}
				// Pop the return value
				return Ok(self.vm.pop());
			}
		}

		Err(())
	}

	/// Gets a reference to the IO object.
	/// Useful if you are using the IO object for some kind of state.
	pub fn get_io(&self) -> &T {
		&self.vm.stdio
	}

	/// Gets a mutable reference to the IO object.
	/// Useful if you are using the IO object for some kind of state.
	pub fn get_io_mut(&mut self) -> &mut T {
		&mut self.vm.stdio
	}
}