1
0

Continue new VM implementation; Write docs

This commit is contained in:
Jeff 2024-12-17 16:31:32 -05:00
parent 4527f7b6ef
commit 72365cd399
25 changed files with 454 additions and 354 deletions

12
Cargo.lock generated
View File

@ -102,15 +102,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -141,7 +132,7 @@ version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"textwrap",
"unicode-width",
]
@ -314,7 +305,6 @@ name = "dust-lang"
version = "0.5.0"
dependencies = [
"annotate-snippets",
"bitflags 2.6.0",
"colored",
"criterion",
"getrandom",

View File

@ -4,9 +4,11 @@ use std::{fs::read_to_string, path::PathBuf};
use clap::{Args, Parser};
use colored::Colorize;
use dust_lang::{compile, CompileError, Compiler, DustError, Lexer, Span, Token, Vm};
use dust_lang::{compile, CompileError, Compiler, DustError, DustString, Lexer, Span, Token, Vm};
use log::{Level, LevelFilter};
const DEFAULT_PROGRAM_NAME: &str = "Dust CLI Input";
#[derive(Parser)]
#[clap(
name = env!("CARGO_PKG_NAME"),
@ -196,7 +198,7 @@ fn main() {
}
}
let chunk = compiler.finish();
let chunk = compiler.finish(Some(DEFAULT_PROGRAM_NAME));
let compile_end = start_time.elapsed();
let vm = Vm::new(chunk);

View File

@ -22,7 +22,6 @@ smallvec = { version = "1.13.2", features = ["const_generics", "serde"] }
smartstring = { version = "1.0.1", features = [
"serde",
], default-features = false }
bitflags = { version = "2.6.0", features = ["serde"] }
[dev-dependencies]
criterion = { version = "0.3.4", features = ["html_reports"] }

View File

@ -263,7 +263,7 @@ impl<'a, W: Write> Disassembler<'a, W> {
self.write_char(border[1])?;
}
self.write_char(border[2]);
self.write_char(border[2])?;
self.write_char('\n')
}
@ -377,7 +377,7 @@ impl<'a, W: Write> Disassembler<'a, W> {
}
pub fn write_prototype_section(&mut self) -> Result<(), io::Error> {
self.write_center_border_bold("Functions");
self.write_center_border_bold("Functions")?;
for chunk in &self.chunk.prototypes {
chunk

View File

@ -27,7 +27,8 @@ use std::io::Write;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::{DustString, FunctionType, Instruction, Span, Value};
use crate::vm::{Record, Register, RunAction};
use crate::{DustString, Function, FunctionType, Instruction, Span, Value};
/// Representation of a Dust program or function.
///
@ -44,6 +45,7 @@ pub struct Chunk {
prototypes: Vec<Chunk>,
stack_size: usize,
record_index: u8,
}
impl Chunk {
@ -56,6 +58,7 @@ impl Chunk {
locals: impl Into<SmallVec<[Local; 8]>>,
prototypes: impl Into<Vec<Chunk>>,
stack_size: usize,
record_index: u8,
) -> Self {
Self {
name,
@ -66,6 +69,7 @@ impl Chunk {
locals: locals.into(),
prototypes: prototypes.into(),
stack_size,
record_index,
}
}
@ -88,31 +92,47 @@ impl Chunk {
locals: locals.into(),
prototypes,
stack_size: 0,
record_index: 0,
}
}
pub fn take_data(
self,
) -> (
Option<DustString>,
FunctionType,
SmallVec<[Instruction; 32]>,
SmallVec<[Span; 32]>,
SmallVec<[Value; 16]>,
SmallVec<[Local; 8]>,
Vec<Chunk>,
usize,
) {
(
pub fn as_function(&self) -> Function {
Function {
name: self.name.clone(),
r#type: self.r#type.clone(),
record_index: self.record_index,
}
}
pub fn into_records(self, records: &mut Vec<Record>) {
let actions = self.instructions().iter().map(RunAction::from).collect();
let record = Record::new(
actions,
None,
self.name,
self.r#type,
self.instructions,
self.positions,
self.constants,
self.locals,
self.prototypes,
self.stack_size,
)
self.record_index,
);
if records.is_empty() {
records.push(record);
for chunk in self.prototypes {
chunk.into_records(records);
}
} else {
for chunk in self.prototypes {
chunk.into_records(records);
}
debug_assert!(record.index() as usize == records.len());
records.push(record);
}
}
pub fn name(&self) -> Option<&DustString> {

View File

@ -20,7 +20,7 @@ pub enum CompileError {
},
// Parsing errors
CannotChainComparison {
ComparisonChain {
position: Span,
},
ExpectedBoolean {
@ -176,7 +176,7 @@ impl AnnotatedError for CompileError {
match self {
Self::CannotAddArguments { .. } => "Cannot add these types",
Self::CannotAddType { .. } => "Cannot add to this type",
Self::CannotChainComparison { .. } => "Cannot chain comparison operations",
Self::ComparisonChain { .. } => "Cannot chain comparison operations",
Self::CannotDivideArguments { .. } => "Cannot divide these types",
Self::CannotDivideType { .. } => "Cannot divide this type",
Self::CannotModuloArguments { .. } => "Cannot modulo these types",

View File

@ -1,9 +1,12 @@
//! Compilation tools and errors
//! The Dust compiler and its accessories.
//!
//! This module provides two compilation options:
//! - [`compile`] borrows a string and returns a chunk, handling the entire compilation process and
//! turning any resulting [`ComplileError`] into a [`DustError`].
//! - [`Compiler`] uses a lexer to get tokens and assembles a chunk.
//! - [`compile`] is a simple function that borrows a string and returns a chunk, handling
//! compilation and turning any resulting error into a [`DustError`], which can easily display a
//! detailed report. The main chunk will be named "main".
//! - [`Compiler`] is created with a [`Lexer`] and protentially emits a [`CompileError`] or
//! [`LexError`] if the input is invalid. Allows passing a name for the main chunk when
//! [`Compiler::finish`] is called.
mod error;
mod optimize;
@ -15,12 +18,12 @@ use std::{
};
use colored::Colorize;
use optimize::{optimize_test_with_explicit_booleans, optimize_test_with_loader_arguments};
use optimize::control_flow_register_consolidation;
use smallvec::{smallvec, SmallVec};
use crate::{
instruction::{
CallNative, Close, GetLocal, Jump, LoadList, Negate, Not, Point, Return, SetLocal, Test,
CallNative, Close, GetLocal, Jump, LoadList, Negate, Not, Return, SetLocal, Test,
},
Argument, Chunk, ConcreteValue, DustError, DustString, FunctionType, Instruction, Lexer, Local,
NativeFunction, Operation, Scope, Span, Token, TokenKind, Type, Value,
@ -35,7 +38,7 @@ use crate::{
/// let source = "40 + 2 == 42";
/// let chunk = compile(source).unwrap();
///
/// assert_eq!(chunk.len(), 3);
/// assert_eq!(chunk.instructions().len(), 3);
/// ```
pub fn compile(source: &str) -> Result<Chunk, DustError> {
let lexer = Lexer::new(source);
@ -45,12 +48,14 @@ pub fn compile(source: &str) -> Result<Chunk, DustError> {
.compile()
.map_err(|error| DustError::compile(error, source))?;
let chunk = compiler.finish();
let name = DustString::from("main");
let chunk = compiler.finish(Some(name));
Ok(chunk)
}
/// Tool for compiling the input a token at a time while assembling a chunk.
/// The Dust compiler assembles a [`Chunk`] for the Dust VM. Any unrecognized symbols, disallowed
/// syntax or conflicting type usage will result in an error.
///
/// See the [`compile`] function an example of how to create and use a Compiler.
#[derive(Debug)]
@ -58,9 +63,10 @@ pub struct Compiler<'src> {
/// Used to get tokens for the compiler.
lexer: Lexer<'src>,
/// Name of the function or program being compiled. This is assigned to the chunk when
/// [`Compiler::finish`] is called.
self_name: Option<DustString>,
/// Name of the function being compiled. This is used to identify recursive calls, so it should
/// be `None` for the main chunk. The main chunk can still be named by passing a name to
/// [`Compiler::finish`], which will override this value.
function_name: Option<DustString>,
/// Type of the function being compiled. This is assigned to the chunk when [`Compiler::finish`]
/// is called.
@ -116,6 +122,7 @@ pub struct Compiler<'src> {
}
impl<'src> Compiler<'src> {
/// Creates a new compiler with the given lexer.
pub fn new(mut lexer: Lexer<'src>) -> Result<Self, CompileError> {
let (current_token, current_position) = lexer.next_token()?;
@ -126,7 +133,7 @@ impl<'src> Compiler<'src> {
);
Ok(Compiler {
self_name: None,
function_name: None,
r#type: FunctionType {
type_parameters: None,
value_parameters: None,
@ -150,7 +157,12 @@ impl<'src> Compiler<'src> {
})
}
pub fn finish(self) -> Chunk {
/// Creates a new chunk with the compiled data, optionally assigning a name to the chunk.
///
/// Note for maintainers: Do not give a name when compiling functions, only the main chunk. This
/// will allow [`Compiler::function_name`] to be both the name used for recursive calls and the
/// name of the function when it is compiled. The name can later be seen in the VM's call stack.
pub fn finish(self, name: Option<impl Into<DustString>>) -> Chunk {
log::info!("End chunk");
let (instructions, positions): (SmallVec<[Instruction; 32]>, SmallVec<[Span; 32]>) = self
@ -163,9 +175,12 @@ impl<'src> Compiler<'src> {
.into_iter()
.map(|(local, _)| local)
.collect::<SmallVec<[Local; 8]>>();
let chunk_name = name
.map(|into_name| into_name.into())
.or(self.function_name);
Chunk::new(
self.self_name,
chunk_name,
self.r#type,
instructions,
positions,
@ -173,9 +188,13 @@ impl<'src> Compiler<'src> {
locals,
self.prototypes,
self.stack_size,
self.record_index,
)
}
/// Compiles the source while checking for errors and returning a [`CompileError`] if any are
/// found. After calling this function, check its return value for an error, then call
/// [`Compiler::finish`] to get the compiled chunk.
pub fn compile(&mut self) -> Result<(), CompileError> {
loop {
self.parse(Precedence::None)?;
@ -852,7 +871,7 @@ impl<'src> Compiler<'src> {
if let Some([Operation::EQUAL | Operation::LESS | Operation::LESS_EQUAL, _, _]) =
self.get_last_operations()
{
return Err(CompileError::CannotChainComparison {
return Err(CompileError::ComparisonChain {
position: self.current_position,
});
}
@ -915,13 +934,13 @@ impl<'src> Compiler<'src> {
}
};
let jump = Instruction::jump(1, true);
let load_false = Instruction::load_boolean(destination, false, true);
let load_true = Instruction::load_boolean(destination, true, false);
let load_true = Instruction::load_boolean(destination, true, true);
let load_false = Instruction::load_boolean(destination, false, false);
self.emit_instruction(comparison, Type::Boolean, operator_position);
self.emit_instruction(jump, Type::None, operator_position);
self.emit_instruction(load_false, Type::Boolean, operator_position);
self.emit_instruction(load_true, Type::Boolean, operator_position);
self.emit_instruction(load_false, Type::Boolean, operator_position);
Ok(())
}
@ -932,29 +951,19 @@ impl<'src> Compiler<'src> {
Some([Operation::TEST, Operation::JUMP, _])
);
let (mut left_instruction, left_type, left_position) = self.pop_last_instruction()?;
let (left_instruction, left_type, left_position) = self.pop_last_instruction()?;
let (left, _) = self.handle_binary_argument(&left_instruction)?;
if is_logic_chain {
let destination = self
.instructions
.iter()
.rev()
.nth(2)
.map_or(0, |(instruction, _, _)| instruction.a_field());
// if is_logic_chain {
// let destination = self
// .instructions
// .iter()
// .rev()
// .nth(2)
// .map_or(0, |(instruction, _, _)| instruction.a_field());
left_instruction.set_a_field(destination);
}
let jump_index = self.instructions.len().saturating_sub(1);
let mut jump_distance = if is_logic_chain {
self.instructions.pop().map_or(0, |(jump, _, _)| {
let Jump { offset, .. } = Jump::from(&jump);
offset
})
} else {
0
};
// left_instruction.set_a_field(destination);
// }
if !left_instruction.yields_value() {
return Err(CompileError::ExpectedExpression {
@ -963,10 +972,19 @@ impl<'src> Compiler<'src> {
});
}
let (left, _) = self.handle_binary_argument(&left_instruction)?;
// self.instructions
// .push((left_instruction, left_type.clone(), left_position));
self.instructions
.push((left_instruction, left_type.clone(), left_position));
// let short_circuit_jump_index = self.instructions.len();
// let mut short_circuit_jump_distance = if is_logic_chain {
// self.instructions.pop().map_or(0, |(jump, _, _)| {
// let Jump { offset, .. } = Jump::from(&jump);
// offset
// })
// } else {
// 1
// };
let operator = self.current_token;
let operator_position = self.current_position;
@ -986,20 +1004,28 @@ impl<'src> Compiler<'src> {
self.advance()?;
self.emit_instruction(test, Type::None, operator_position);
self.emit_instruction(Instruction::jump(1, true), Type::None, operator_position);
let jump_index = self.instructions.len();
self.parse_sub_expression(&rule.precedence)?;
let jump_distance = (self.instructions.len() - jump_index) as u8;
let jump = Instruction::jump(jump_distance, true);
self.instructions
.insert(jump_index, (jump, Type::None, operator_position));
let (mut right_instruction, _, _) = self.instructions.last_mut().unwrap();
right_instruction.set_a_field(left_instruction.a_field());
if is_logic_chain {
let expression_length = self.instructions.len() - jump_index - 1;
jump_distance += expression_length as u8;
let jump = Instruction::jump(jump_distance, true);
// short_circuit_jump_distance += (self.instructions.len() - short_circuit_jump_index) as u8;
// let jump = Instruction::jump(short_circuit_jump_distance, true);
self.instructions
.insert(jump_index, (jump, Type::None, operator_position));
}
// self.instructions.insert(
// short_circuit_jump_index,
// (jump, Type::None, operator_position),
// );
Ok(())
}
@ -1021,7 +1047,7 @@ impl<'src> Compiler<'src> {
local_index
} else if let Some(native_function) = NativeFunction::from_str(identifier) {
return self.parse_call_native(native_function);
} else if self.self_name.as_deref() == Some(identifier) {
} else if self.function_name.as_deref() == Some(identifier) {
let destination = self.next_register();
let load_function = Instruction::load_function(destination, self.record_index);
@ -1175,35 +1201,22 @@ impl<'src> Compiler<'src> {
self.advance()?;
self.parse_expression()?;
if matches!(
self.get_last_operations(),
Some([
Operation::EQUAL | Operation::LESS | Operation::LESS_EQUAL,
Operation::JUMP,
Operation::LOAD_BOOLEAN,
Operation::LOAD_BOOLEAN
]),
) {
self.instructions.pop();
self.instructions.pop();
self.instructions.pop();
} else if let Some((instruction, _, _)) = self.instructions.last() {
let argument = match instruction.as_argument() {
Some(argument) => argument,
None => {
return Err(CompileError::ExpectedExpression {
found: self.previous_token.to_owned(),
position: self.previous_position,
});
}
};
let test = Instruction::from(Test {
argument,
test_value: true,
});
let (last_instruction, _, _) = self.instructions.last().unwrap();
let argument = match last_instruction.as_argument() {
Some(argument) => argument,
None => {
return Err(CompileError::ExpectedExpression {
found: self.previous_token.to_owned(),
position: self.previous_position,
});
}
};
let test = Instruction::from(Test {
argument,
test_value: true,
});
self.emit_instruction(test, Type::None, self.current_position)
}
self.emit_instruction(test, Type::None, self.current_position);
let if_block_start = self.instructions.len();
let if_block_start_position = self.current_position;
@ -1211,8 +1224,8 @@ impl<'src> Compiler<'src> {
if let Token::LeftBrace = self.current_token {
self.parse_block()?;
} else {
return Err(CompileError::ExpectedToken {
expected: TokenKind::LeftBrace,
return Err(CompileError::ExpectedTokenMultiple {
expected: &[TokenKind::If, TokenKind::LeftBrace],
found: self.current_token.to_owned(),
position: self.current_position,
});
@ -1235,15 +1248,11 @@ impl<'src> Compiler<'src> {
position: self.current_position,
});
}
true
} else if if_block_type != Type::None {
return Err(CompileError::IfMissingElse {
position: Span(if_block_start_position.0, self.current_position.1),
});
} else {
false
};
}
let else_block_end = self.instructions.len();
let else_block_distance = (else_block_end - if_block_end) as u8;
@ -1288,25 +1297,18 @@ impl<'src> Compiler<'src> {
}
}
let jump = Instruction::from(Jump {
offset: if_block_distance,
is_positive: true,
});
let jump = Instruction::jump(if_block_distance, true);
self.instructions
.insert(if_block_start, (jump, Type::None, if_block_start_position));
optimize_test_with_explicit_booleans(self);
optimize_test_with_loader_arguments(self);
control_flow_register_consolidation(self);
let else_last_register = self.next_register().saturating_sub(1);
let r#move = Instruction::from(Point {
from: else_last_register,
to: if_last_register,
});
let point = Instruction::point(else_last_register, if_last_register);
if if_last_register < else_last_register {
self.emit_instruction(r#move, Type::None, self.current_position);
self.emit_instruction(point, else_block_type, self.current_position);
}
Ok(())
@ -1490,9 +1492,7 @@ impl<'src> Compiler<'src> {
fn parse_implicit_return(&mut self) -> Result<(), CompileError> {
if self.allow(Token::Semicolon)? {
let r#return = Instruction::from(Return {
should_return_value: false,
});
let r#return = Instruction::r#return(false);
self.emit_instruction(r#return, Type::None, self.current_position);
} else {
@ -1507,9 +1507,7 @@ impl<'src> Compiler<'src> {
}
});
let should_return_value = previous_expression_type != Type::None;
let r#return = Instruction::from(Return {
should_return_value,
});
let r#return = Instruction::r#return(should_return_value);
self.update_return_type(previous_expression_type.clone())?;
self.emit_instruction(r#return, Type::None, self.current_position);
@ -1577,7 +1575,7 @@ impl<'src> Compiler<'src> {
function_compiler.advance()?;
function_compiler.self_name = Some(text.into());
function_compiler.function_name = Some(text.into());
Some((text, position))
} else {
@ -1664,7 +1662,8 @@ impl<'src> Compiler<'src> {
self.lexer.skip_to(self.current_position.1);
let function_end = function_compiler.previous_position.1;
let chunk = function_compiler.finish();
let record_index = function_compiler.record_index;
let chunk = function_compiler.finish(None::<&str>);
let destination = self.next_register();
self.prototypes.push(chunk);
@ -1679,7 +1678,7 @@ impl<'src> Compiler<'src> {
);
}
let load_function = Instruction::load_function(destination, self.record_index);
let load_function = Instruction::load_function(destination, record_index);
self.emit_instruction(
load_function,
@ -1703,7 +1702,10 @@ impl<'src> Compiler<'src> {
position: self.previous_position,
})?;
if last_instruction.operation() != Operation::LOAD_FUNCTION {
if !matches!(
last_instruction_type,
Type::Function(_) | Type::SelfFunction
) {
return Err(CompileError::ExpectedFunction {
found: self.previous_token.to_owned(),
actual_type: last_instruction_type.clone(),
@ -1970,29 +1972,25 @@ impl<'src> Compiler<'src> {
/// Operator precedence levels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Precedence {
None,
Assignment,
Conditional,
LogicalOr,
LogicalAnd,
Equality,
Comparison,
Term,
Factor,
Unary,
Call,
Primary,
Primary = 9,
Call = 8,
Unary = 7,
Factor = 6,
Term = 5,
Comparison = 4,
LogicalAnd = 3,
LogicalOr = 2,
Assignment = 1,
None = 0,
}
impl Precedence {
fn increment(&self) -> Self {
match self {
Precedence::None => Precedence::Assignment,
Precedence::Assignment => Precedence::Conditional,
Precedence::Conditional => Precedence::LogicalOr,
Precedence::Assignment => Precedence::LogicalOr,
Precedence::LogicalOr => Precedence::LogicalAnd,
Precedence::LogicalAnd => Precedence::Equality,
Precedence::Equality => Precedence::Comparison,
Precedence::LogicalAnd => Precedence::Comparison,
Precedence::Comparison => Precedence::Term,
Precedence::Term => Precedence::Factor,
Precedence::Factor => Precedence::Unary,
@ -2036,7 +2034,7 @@ impl From<&Token<'_>> for ParseRule<'_> {
Token::BangEqual => ParseRule {
prefix: None,
infix: Some(Compiler::parse_comparison_binary),
precedence: Precedence::Equality,
precedence: Precedence::Comparison,
},
Token::Bool => ParseRule {
prefix: Some(Compiler::expect_expression),
@ -2082,7 +2080,7 @@ impl From<&Token<'_>> for ParseRule<'_> {
Token::DoubleEqual => ParseRule {
prefix: None,
infix: Some(Compiler::parse_comparison_binary),
precedence: Precedence::Equality,
precedence: Precedence::Comparison,
},
Token::DoublePipe => ParseRule {
prefix: None,

View File

@ -1,15 +1,11 @@
//! Functions used by the compiler to optimize a chunk's bytecode during compilation.
use crate::{Compiler, Operation};
use crate::{Compiler, Instruction, Operation};
/// Optimizes a control flow pattern by removing redundant instructions.
/// Optimizes a control flow pattern to use fewer registers and avoid using a `POINT` instruction.
/// Use this after parsing an if/else statement.
///
/// If a comparison instruction is followed by a test instruction, the test instruction may be
/// redundant because the comparison instruction already sets the correct value. If the test's
/// arguments (i.e. the boolean loaders) are `true` and `false` (in that order) then the boolean
/// loaders, jump and test instructions are removed, leaving a single comparison instruction.
///
/// This makes the following two code snippets compile to the same bytecode:
/// This makes the following examples compile to the same bytecode:
///
/// ```dust
/// 4 == 4
@ -19,58 +15,27 @@ use crate::{Compiler, Operation};
/// if 4 == 4 { true } else { false }
/// ```
///
/// The instructions must be in the following order:
/// - `EQUAL`, `LESS` or `LESS_EQUAL`
/// - `TEST`
/// - `JUMP`
/// - `LOAD_BOOLEAN`
/// - `LOAD_BOOLEAN`
pub fn optimize_test_with_explicit_booleans(compiler: &mut Compiler) {
if matches!(
compiler.get_last_operations(),
Some([
Operation::EQUAL | Operation::LESS | Operation::LESS_EQUAL,
Operation::TEST,
Operation::JUMP,
Operation::LOAD_BOOLEAN,
Operation::LOAD_BOOLEAN,
])
) {
log::debug!("Removing redundant test, jump and boolean loaders after comparison");
let first_loader = compiler.instructions.iter().nth_back(1).unwrap();
let second_loader = compiler.instructions.last().unwrap();
let first_boolean = first_loader.0.b_field() != 0;
let second_boolean = second_loader.0.b_field() != 0;
if first_boolean && !second_boolean {
compiler.instructions.pop();
compiler.instructions.pop();
compiler.instructions.pop();
compiler.instructions.pop();
}
}
}
/// Optimizes a control flow pattern.
///
/// TEST instructions (which are always followed by a JUMP) can be optimized when the next
/// instructions are two constant or boolean loaders. The first loader is set to skip an instruction
/// if it is run while the second loader is modified to use the first's register. Foregoing the use
/// a jump instruction is an optimization but consolidating the registers is a necessity. This is
/// because test instructions are essentially control flow and a subsequent SET_LOCAL instruction
/// would not know at compile time which branch would be executed at runtime.
/// When they occur in the sequence shown below, instructions can be optimized by taking advantage
/// of the loaders' ability to skip an instruction after loading a value. If these instructions are
/// the result of a binary expression, this will not change anything because they were already
/// emitted optimally. Control flow patterns, however, can be optimized because the load
/// instructions are from seperate expressions that each uses its own register. Since only one of
/// the two branches will be executed, this is wasteful. It would also require the compiler to emit
/// a `POINT` instruction to prevent the VM from encountering an empty register.
///
/// The instructions must be in the following order:
/// - `TEST`
/// - `EQUAL` | `LESS` | `LESS_EQUAL` | `TEST`
/// - `JUMP`
/// - `LOAD_BOOLEAN` or `LOAD_CONSTANT`
/// - `LOAD_BOOLEAN` or `LOAD_CONSTANT`
pub fn optimize_test_with_loader_arguments(compiler: &mut Compiler) {
///
/// This optimization was taken from `A No-Frills Introduction to Lua 5.1 VM Instructions` by
/// Kein-Hong Man.
pub fn control_flow_register_consolidation(compiler: &mut Compiler) {
if !matches!(
compiler.get_last_operations(),
Some([
Operation::TEST,
Operation::EQUAL | Operation::LESS | Operation::LESS_EQUAL | Operation::TEST,
Operation::JUMP,
Operation::LOAD_BOOLEAN | Operation::LOAD_CONSTANT,
Operation::LOAD_BOOLEAN | Operation::LOAD_CONSTANT,
@ -81,12 +46,17 @@ pub fn optimize_test_with_loader_arguments(compiler: &mut Compiler) {
log::debug!("Consolidating registers for control flow optimization");
let first_loader = &mut compiler.instructions.iter_mut().nth_back(1).unwrap().0;
first_loader.set_c_field(true as u8);
let first_loader_index = compiler.instructions.len() - 2;
let (first_loader, _, _) = &mut compiler.instructions.get_mut(first_loader_index).unwrap();
let first_loader_destination = first_loader.a_field();
let second_loader = &mut compiler.instructions.last_mut().unwrap().0;
*first_loader =
Instruction::load_boolean(first_loader.a_field(), first_loader.b_field() != 0, true);
second_loader.set_a_field(first_loader_destination);
let second_loader_index = compiler.instructions.len() - 1;
let (second_loader, _, _) = &mut compiler.instructions.get_mut(second_loader_index).unwrap();
*second_loader = Instruction::load_boolean(
first_loader_destination,
second_loader.b_field() != 0,
false,
);
}

View File

@ -26,7 +26,20 @@ impl<'src> DustError<'src> {
}
pub fn report(&self) -> String {
let (title, description, detail_snippets, help_snippets) = self.error_data();
let (title, description, detail_snippets, help_snippets) = match self {
Self::Compile { error, .. } => (
CompileError::title(),
error.description(),
error.detail_snippets(),
error.help_snippets(),
),
Self::NativeFunction { error, .. } => (
NativeFunctionError::title(),
error.description(),
error.detail_snippets(),
error.help_snippets(),
),
};
let label = format!("{}: {}", title, description);
let message = Level::Error
.title(&label)
@ -46,30 +59,6 @@ impl<'src> DustError<'src> {
report
}
fn error_data(
&self,
) -> (
&str,
&str,
SmallVec<[(String, Span); 2]>,
SmallVec<[(String, Span); 2]>,
) {
match self {
Self::Compile { error, .. } => (
CompileError::title(),
error.description(),
error.detail_snippets(),
error.help_snippets(),
),
Self::NativeFunction { error, .. } => (
NativeFunctionError::title(),
error.description(),
error.detail_snippets(),
error.help_snippets(),
),
}
}
fn source(&self) -> &str {
match self {
Self::Compile { source, .. } => source,

View File

@ -44,6 +44,6 @@ impl From<LoadFunction> for Instruction {
impl Display for LoadFunction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "R{} = P{}", self.destination, self.record_index)
write!(f, "R{} = F{}", self.destination, self.record_index)
}
}

View File

@ -105,11 +105,11 @@ mod load_function;
mod load_list;
mod load_self;
mod modulo;
mod r#move;
mod multiply;
mod negate;
mod not;
mod operation;
mod point;
mod r#return;
mod set_local;
mod subtract;
@ -136,7 +136,7 @@ pub use multiply::Multiply;
pub use negate::Negate;
pub use not::Not;
pub use operation::Operation;
pub use r#move::Point;
pub use point::Point;
pub use r#return::Return;
pub use set_local::SetLocal;
pub use subtract::Subtract;
@ -475,7 +475,8 @@ impl Instruction {
pub fn yields_value(&self) -> bool {
match self.operation() {
Operation::LOAD_BOOLEAN
Operation::POINT
| Operation::LOAD_BOOLEAN
| Operation::LOAD_CONSTANT
| Operation::LOAD_FUNCTION
| Operation::LOAD_LIST
@ -488,18 +489,17 @@ impl Instruction {
| Operation::MODULO
| Operation::NEGATE
| Operation::NOT
| Operation::EQUAL
| Operation::LESS
| Operation::LESS_EQUAL
| Operation::CALL => true,
Operation::CALL_NATIVE => {
let function = NativeFunction::from(self.b_field());
function.returns_value()
}
Operation::POINT
| Operation::CLOSE
Operation::CLOSE
| Operation::SET_LOCAL
| Operation::EQUAL
| Operation::LESS
| Operation::LESS_EQUAL
| Operation::TEST
| Operation::TEST_SET
| Operation::JUMP
@ -688,17 +688,17 @@ impl Instruction {
Operation::CALL => {
let Call {
destination,
function_register: record_index,
function_register,
argument_count,
} = Call::from(self);
let arguments_start = destination.saturating_sub(argument_count);
match argument_count {
0 => format!("R{destination} = P{record_index}()"),
1 => format!("R{destination} = P{record_index}(R{arguments_start})"),
0 => format!("R{destination} = R{function_register}()"),
1 => format!("R{destination} = R{function_register}(R{arguments_start})"),
_ => {
format!(
"R{destination} = P{record_index}(R{arguments_start}..R{destination})"
"R{destination} = R{function_register}(R{arguments_start}..R{destination})"
)
}
}

View File

@ -39,7 +39,7 @@ impl Operation {
impl Operation {
pub fn name(&self) -> &'static str {
match *self {
Self::POINT => "MOVE",
Self::POINT => "POINT",
Self::CLOSE => "CLOSE",
Self::LOAD_BOOLEAN => "LOAD_BOOLEAN",
Self::LOAD_CONSTANT => "LOAD_CONSTANT",

View File

@ -17,7 +17,7 @@ pub fn panic(
let value = record.open_register(register_index);
if let Some(string) = value.as_string() {
message.push_str(&string);
message.push_str(string);
message.push('\n');
}
}

View File

@ -314,12 +314,12 @@ impl Display for FunctionType {
write!(f, "(")?;
if let Some(value_parameters) = &self.value_parameters {
for (index, (identifier, r#type)) in value_parameters.iter().enumerate() {
for (index, (_, r#type)) in value_parameters.iter().enumerate() {
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{identifier}: {type}")?;
write!(f, "{type}")?;
}
}

View File

@ -91,8 +91,16 @@ impl ConcreteValue {
Integer(sum)
}
(String(left), Character(_)) => todo!(),
(String(left), String(right)) => todo!(),
(String(left), Character(right)) => {
let concatenated = format!("{}{}", left, right);
String(DustString::from(concatenated))
}
(String(left), String(right)) => {
let concatenated = format!("{}{}", left, right);
String(DustString::from(concatenated))
}
_ => panic!(
"{}",
ValueError::CannotAdd(

View File

@ -8,8 +8,7 @@ use super::DustString;
pub struct Function {
pub name: Option<DustString>,
pub r#type: FunctionType,
pub record_index: usize,
pub prototype_index: usize,
pub record_index: u8,
}
impl Display for Function {

View File

@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug, Display, Formatter};
use crate::{vm::Record, Type, Vm};
use crate::{vm::Record, Type};
#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum Value {
@ -23,9 +23,6 @@ pub enum Value {
#[serde(skip)]
Function(Function),
#[serde(skip)]
SelfFunction,
}
impl Value {
@ -60,7 +57,6 @@ impl Value {
Type::List(Box::new(item_type.clone()))
}
Value::Function(Function { r#type, .. }) => Type::Function(Box::new(r#type.clone())),
Value::SelfFunction => Type::SelfFunction,
}
}
@ -163,7 +159,6 @@ impl Value {
Value::AbstractList(list) => list.display(record),
Value::Concrete(concrete_value) => concrete_value.display(),
Value::Function(function) => DustString::from(function.to_string()),
Value::SelfFunction => DustString::from("self"),
}
}
}
@ -174,7 +169,6 @@ impl Display for Value {
Value::Concrete(concrete_value) => write!(f, "{concrete_value}"),
Value::AbstractList(list) => write!(f, "{list}"),
Value::Function(function) => write!(f, "{function}"),
Value::SelfFunction => write!(f, "self"),
}
}
}

View File

@ -1,10 +1,12 @@
use std::fmt::{self, Debug, Display, Formatter};
use super::{FunctionCall, VmError};
use crate::DustString;
use super::VmError;
#[derive(Clone, PartialEq)]
pub struct CallStack {
pub calls: Vec<FunctionCall>,
calls: Vec<FunctionCall>,
}
impl CallStack {
@ -47,6 +49,18 @@ impl CallStack {
self.calls.last().unwrap()
}
pub fn last_mut_or_panic(&mut self) -> &mut FunctionCall {
assert!(!self.is_empty(), "{}", VmError::CallStackUnderflow);
self.calls.last_mut().unwrap()
}
}
impl Default for CallStack {
fn default() -> Self {
Self::new()
}
}
impl Debug for CallStack {
@ -60,9 +74,37 @@ impl Display for CallStack {
writeln!(f, "-- DUST CALL STACK --")?;
for function_call in &self.calls {
writeln!(f, "{function_call:?}")?;
writeln!(f, "{function_call}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FunctionCall {
pub name: Option<DustString>,
pub record_index: u8,
pub return_register: u8,
pub ip: usize,
}
impl Display for FunctionCall {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let FunctionCall {
name,
record_index,
return_register,
..
} = self;
let name = name
.as_ref()
.map(|name| name.as_str())
.unwrap_or("anonymous");
write!(
f,
"{name} (Record: {record_index}, Return register: {return_register})"
)
}
}

View File

@ -1,6 +1,6 @@
use std::fmt::{self, Display, Formatter};
use crate::{DustString, InstructionData, Value};
use crate::{InstructionData, Value};
use super::call_stack::CallStack;

View File

@ -2,7 +2,7 @@
mod call_stack;
mod error;
mod record;
mod runner;
mod run_action;
mod thread;
use std::{
@ -11,9 +11,10 @@ use std::{
thread::spawn,
};
pub use call_stack::CallStack;
pub use call_stack::{CallStack, FunctionCall};
pub use error::VmError;
pub use record::Record;
pub use run_action::RunAction;
pub use thread::{Thread, ThreadSignal};
use crate::{compile, Chunk, DustError, Value};
@ -92,10 +93,3 @@ impl Display for Pointer {
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FunctionCall {
record_index: usize,
return_register: u8,
argument_count: u8,
}

View File

@ -2,41 +2,63 @@ use std::mem::replace;
use smallvec::SmallVec;
use crate::{Local, Span, Value};
use crate::{DustString, Function, FunctionType, Local, Span, Value};
use super::{runner::RunAction, Pointer, Register};
use super::{run_action::RunAction, Pointer, Register};
pub struct Record {
pub ip: usize,
pub actions: SmallVec<[RunAction; 32]>,
positions: SmallVec<[Span; 32]>,
stack: Vec<Register>,
last_assigned_register: Option<u8>,
name: Option<DustString>,
r#type: FunctionType,
positions: SmallVec<[Span; 32]>,
constants: SmallVec<[Value; 16]>,
locals: SmallVec<[Local; 8]>,
last_assigned_register: Option<u8>,
stack_size: usize,
index: u8,
}
impl Record {
pub fn new(
stack: Vec<Register>,
actions: SmallVec<[RunAction; 32]>,
last_assigned_register: Option<u8>,
name: Option<DustString>,
r#type: FunctionType,
positions: SmallVec<[Span; 32]>,
constants: SmallVec<[Value; 16]>,
locals: SmallVec<[Local; 8]>,
actions: SmallVec<[RunAction; 32]>,
positions: SmallVec<[Span; 32]>,
stack_size: usize,
index: u8,
) -> Self {
Self {
ip: 0,
actions,
stack: vec![Register::Empty; stack_size],
last_assigned_register,
name,
r#type,
positions,
stack,
constants,
locals,
last_assigned_register: None,
stack_size,
index,
}
}
pub fn name(&self) -> Option<&DustString> {
self.name.as_ref()
}
pub fn index(&self) -> u8 {
self.index
}
pub fn stack_size(&self) -> usize {
self.stack.len()
}
@ -49,6 +71,14 @@ impl Record {
self.last_assigned_register
}
pub fn as_function(&self) -> Function {
Function {
name: self.name.clone(),
r#type: self.r#type.clone(),
record_index: self.index,
}
}
pub(crate) fn follow_pointer(&self, pointer: Pointer) -> &Value {
log::trace!("Follow pointer {pointer}");

View File

@ -1,15 +1,12 @@
use smallvec::SmallVec;
use crate::{
instruction::{
Call, CallNative, Close, LoadBoolean, LoadConstant, LoadFunction, LoadList, LoadSelf, Point,
},
vm::VmError,
AbstractList, ConcreteValue, Function, Instruction, InstructionData, NativeFunction, Type,
Value,
AbstractList, ConcreteValue, Instruction, InstructionData, Type, Value,
};
use super::{thread::ThreadSignal, FunctionCall, Pointer, Record, Register};
use super::{thread::ThreadSignal, Pointer, Record, Register};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RunAction {
@ -122,6 +119,8 @@ pub fn load_constant(instruction_data: InstructionData, record: &mut Record) ->
} = instruction_data.into();
let register = Register::Pointer(Pointer::Constant(constant_index));
log::trace!("Load constant {constant_index} into R{destination}");
record.set_register(destination, register);
if jump_next {
@ -170,18 +169,22 @@ pub fn load_list(instruction_data: InstructionData, record: &mut Record) -> Thre
ThreadSignal::Continue
}
pub fn load_function(instruction_data: InstructionData, record: &mut Record) -> ThreadSignal {
pub fn load_function(instruction_data: InstructionData, _: &mut Record) -> ThreadSignal {
let LoadFunction {
destination,
record_index,
} = instruction_data.into();
ThreadSignal::Continue
ThreadSignal::LoadFunction {
from_record_index: record_index,
to_register_index: destination,
}
}
pub fn load_self(instruction_data: InstructionData, record: &mut Record) -> ThreadSignal {
let LoadSelf { destination } = instruction_data.into();
let register = Register::Value(Value::SelfFunction);
let function = record.as_function();
let register = Register::Value(Value::Function(function));
record.set_register(destination, register);
@ -510,9 +513,9 @@ pub fn jump(instruction_data: InstructionData, record: &mut Record) -> ThreadSig
let is_positive = c != 0;
if is_positive {
record.ip += offset + 1
record.ip += offset;
} else {
record.ip -= offset
record.ip -= offset + 1;
}
ThreadSignal::Continue
@ -535,11 +538,11 @@ pub fn call(instruction_data: InstructionData, record: &mut Record) -> ThreadSig
),
};
ThreadSignal::Call(FunctionCall {
ThreadSignal::Call {
record_index: function.record_index,
return_register: destination,
argument_count,
})
}
}
pub fn call_native(instruction_data: InstructionData, record: &mut Record) -> ThreadSignal {
@ -551,12 +554,9 @@ pub fn call_native(instruction_data: InstructionData, record: &mut Record) -> Th
let first_argument_index = destination - argument_count;
let argument_range = first_argument_index..destination;
let function = NativeFunction::from(function);
let thread_signal = function
function
.call(record, Some(destination), argument_range)
.unwrap_or_else(|error| panic!("{error:?}"));
thread_signal
.unwrap_or_else(|error| panic!("{error:?}"))
}
pub fn r#return(instruction_data: InstructionData, _: &mut Record) -> ThreadSignal {

View File

@ -1,27 +1,9 @@
use std::mem::swap;
use crate::{
vm::{FunctionCall, Register},
Chunk, DustString, Value,
};
use crate::{vm::Register, Chunk, Value};
use super::{record::Record, runner::RunAction, CallStack, FunctionCall, VmError};
fn create_records(chunk: Chunk, records: &mut Vec<Record>) {
let (_, _, instructions, positions, constants, locals, prototypes, stack_size) =
chunk.take_data();
let actions = instructions.into_iter().map(RunAction::from).collect();
let record = Record::new(
vec![Register::Empty; stack_size],
constants,
locals,
actions,
positions,
);
for chunk in prototypes {
create_records(chunk, records);
}
records.push(record);
}
use super::{record::Record, CallStack, VmError};
pub struct Thread {
call_stack: CallStack,
@ -33,7 +15,7 @@ impl Thread {
let call_stack = CallStack::with_capacity(chunk.prototypes().len() + 1);
let mut records = Vec::with_capacity(chunk.prototypes().len() + 1);
create_records(chunk, &mut records);
chunk.into_records(&mut records);
Thread {
call_stack,
@ -42,36 +24,104 @@ impl Thread {
}
pub fn run(&mut self) -> Option<Value> {
let (record, remaining_records) = self.records.split_first_mut().unwrap();
let mut active = &mut self.records[0];
log::info!(
"Starting thread with {}",
active
.as_function()
.name
.unwrap_or_else(|| DustString::from("anonymous"))
);
loop {
assert!(
record.ip < record.actions.len(),
active.ip < active.actions.len(),
"{}",
VmError::InstructionIndexOutOfBounds {
call_stack: self.call_stack.clone(),
ip: record.ip,
ip: active.ip,
}
);
let action = record.actions[record.ip];
let signal = (action.logic)(action.data, record);
log::trace!(
"Run \"{}\" | Record = {} | IP = {}",
active
.name()
.cloned()
.unwrap_or_else(|| DustString::from("anonymous")),
active.index(),
active.ip
);
let action = active.actions[active.ip];
let signal = (action.logic)(action.data, active);
active.ip += 1;
match signal {
ThreadSignal::Continue => {
record.ip += 1;
}
ThreadSignal::Call(function_call) => {
swap(record, &mut remaining_records[function_call.record_index]);
ThreadSignal::Continue => {}
ThreadSignal::Call {
record_index,
return_register,
argument_count,
} => {
let record_index = record_index as usize;
let first_argument_register = return_register - argument_count;
let mut arguments = Vec::with_capacity(argument_count as usize);
for register_index in first_argument_register..return_register {
let value = active
.replace_register_or_clone_constant(register_index, Register::Empty);
arguments.push(value);
}
if record_index == active.index() as usize {
log::trace!("Recursion detected");
self.call_stack.last_mut_or_panic().ip = active.ip;
active.ip = 0;
}
active = &mut self.records[record_index];
for (index, argument) in arguments.into_iter().enumerate() {
active.set_register(index as u8, Register::Value(argument));
}
let function_call = FunctionCall {
name: active.name().cloned(),
record_index: active.index(),
return_register,
ip: 0,
};
self.call_stack.push(function_call);
}
ThreadSignal::LoadFunction {
from_record_index,
to_register_index,
} => {
let function_record_index = from_record_index as usize;
let original_record_index = active.index() as usize;
active = &mut self.records[function_record_index];
let function = active.as_function();
let register = Register::Value(Value::Function(function));
active = &mut self.records[original_record_index];
active.set_register(to_register_index, register);
}
ThreadSignal::Return(should_return_value) => {
let returning_call = match self.call_stack.pop() {
Some(function_call) => function_call,
None => {
if should_return_value {
return record.last_assigned_register().map(|register| {
record.replace_register_or_clone_constant(
return active.last_assigned_register().map(|register| {
active.replace_register_or_clone_constant(
register,
Register::Empty,
)
@ -82,19 +132,26 @@ impl Thread {
}
};
let outer_call = self.call_stack.last_or_panic();
let record_index = outer_call.record_index as usize;
log::trace!("Return from {returning_call} to {outer_call}");
if should_return_value {
let return_register = record
let return_register = active
.last_assigned_register()
.unwrap_or_else(|| panic!("Expected return value"));
let value = record
let return_value = active
.replace_register_or_clone_constant(return_register, Register::Empty);
swap(record, &mut remaining_records[outer_call.record_index]);
active = &mut self.records[record_index];
record.set_register(returning_call.return_register, Register::Value(value));
active.set_register(
returning_call.return_register,
Register::Value(return_value),
);
} else {
swap(record, &mut remaining_records[outer_call.record_index]);
active = &mut self.records[record_index];
active.ip = outer_call.ip;
}
}
}
@ -104,6 +161,14 @@ impl Thread {
pub enum ThreadSignal {
Continue,
Call(FunctionCall),
Call {
record_index: u8,
return_register: u8,
argument_count: u8,
},
Return(bool),
LoadFunction {
from_record_index: u8,
to_register_index: u8,
},
}

View File

@ -5,4 +5,4 @@ fn fib (n: int) -> int {
fib(n - 1) + fib(n - 2)
}
write_line(fib(25))
write_line(fib(1))