diff --git a/Cargo.lock b/Cargo.lock index 49f4f98..86146ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "bumpalo" version = "3.16.0" @@ -128,6 +143,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "colorchoice" version = "1.0.3" @@ -144,6 +165,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "dust-cli" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "dust-lang", + "env_logger", + "log", + "postcard", + "serde_json", +] + [[package]] name = "dust-lang" version = "0.5.0" @@ -158,17 +198,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "dust-shell" -version = "0.1.0" -dependencies = [ - "clap", - "colored", - "dust-lang", - "env_logger", - "log", -] - [[package]] name = "env_filter" version = "0.1.2" @@ -205,6 +234,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -250,6 +302,16 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -268,6 +330,17 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "postcard" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +dependencies = [ + "cobs", + "heapless", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -354,12 +427,33 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.214" @@ -382,9 +476,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -392,6 +486,21 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index de66bc3..7de1d01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["dust-lang", "dust-shell"] -default-members = ["dust-lang"] +members = ["dust-lang", "dust-cli"] +default-members = ["dust-lang", "dust-cli"] resolver = "2" [workspace.package] diff --git a/dust-shell/Cargo.toml b/dust-cli/Cargo.toml similarity index 61% rename from dust-shell/Cargo.toml rename to dust-cli/Cargo.toml index f1b5cd0..05812f8 100644 --- a/dust-shell/Cargo.toml +++ b/dust-cli/Cargo.toml @@ -1,15 +1,22 @@ [package] -name = "dust-shell" +name = "dust-cli" +description = "The Dust Programming Language CLI" version = "0.1.0" -authors.workspace = true +authors = ["Jeff Anderson"] edition.workspace = true license.workspace = true readme.workspace = true repository.workspace = true +[[bin]] +name = "dust" +path = "src/main.rs" + [dependencies] clap = { version = "4.5.14", features = ["derive"] } colored = "2.1.0" dust-lang = { path = "../dust-lang" } env_logger = "0.11.5" log = "0.4.22" +postcard = "1.0.10" +serde_json = "1.0.133" diff --git a/dust-cli/src/main.rs b/dust-cli/src/main.rs new file mode 100644 index 0000000..1590c90 --- /dev/null +++ b/dust-cli/src/main.rs @@ -0,0 +1,344 @@ +use std::{ + fmt::{self, Display, Formatter}, + fs::{read_to_string, File, OpenOptions}, + io::{stdin, stdout, Read, Write}, + time::Instant, +}; + +use clap::{Args, Parser, Subcommand, ValueEnum}; +use colored::Colorize; +use dust_lang::{compile, display_token_list, format, lex, run_source, vm::run_chunk, Chunk}; +use env_logger::Target; +use log::{Level, LevelFilter}; + +#[derive(Parser)] +#[command(version, author, about)] +struct Cli { + #[command(flatten)] + input: Input, + + #[command(flatten)] + output: Output, + + /// Log level: INFO, DEBUG, TRACE, WARN or ERROR. This overrides the DUST_LOG environment variable. + #[arg(short, long, value_enum, value_name = "LEVEL", group = "log")] + log_level: Option, + + /// Path to a file for log output + #[arg(long, group = "log")] + log_file: Option, + + /// Which CLI feature to use, defaults to "run" + #[command(subcommand)] + command: Option, +} + +#[derive(Args, Clone)] +#[group(required = true, multiple = true)] +struct Input { + /// Path to a Dust source file. If this and `input` are not provided, input will be read from + /// stdin. + source_file: Option, + + /// Dust source code to compile. If this and `source_file` are not provided, input will be read + /// from stdin. + #[arg(short, long, conflicts_with = "source_file")] + input: Option>, + + /// The format of the input + #[arg(short, long, value_enum, default_value = "source")] + read_format: IoFormat, +} + +#[derive(Args, Clone)] +#[group(required = false, multiple = false)] +struct Output { + /// Path to a file for output. If not provided, output will be written to stdout. + #[arg(short, long)] + output_file: Option, + + /// The format of the output + #[arg(short, long, value_enum, default_value = "postcard")] + write_format: IoFormat, +} + +#[derive(Subcommand, Clone, Copy)] +enum Command { + /// Compile Dust to an intermediate format + Compile, + /// Compile and display the disassembled chunk + Disassemble { + /// Whether to style the output + #[arg(short, long, default_value = "true")] + style: bool, + }, + /// Format and display the source code + Format { + /// Whether to color the output + #[arg(short, long, default_value = "true")] + color: bool, + + /// Number of spaces per indent level + #[arg(short, long, default_value = "4")] + indent: usize, + + /// Whether to include line numbers in the output + #[arg(short, long, default_value = "true")] + line_numbers: bool, + }, + /// Compile and run the Dust code + Run, + /// Lex and display the token list + Tokenize { + /// Whether to style the output + #[arg(short, long, default_value = "true")] + style: bool, + }, +} + +#[derive(ValueEnum, Clone, Copy)] +enum IoFormat { + Json, + Postcard, + Source, +} + +impl Display for IoFormat { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + IoFormat::Json => write!(f, "json"), + IoFormat::Postcard => write!(f, "postcard"), + IoFormat::Source => write!(f, "source"), + } + } +} + +fn main() -> Result<(), String> { + let Cli { + input, + output, + log_level, + log_file, + command, + } = Cli::parse(); + let command = command.unwrap_or(Command::Run); + let start = Instant::now(); + let mut logger = env_logger::builder(); + + logger.format(move |buf, record| { + let level_display = match record.level() { + Level::Info => "INFO".bold().white(), + Level::Debug => "DEBUG".bold().blue(), + Level::Warn => "WARN".bold().yellow(), + Level::Error => "ERROR".bold().red(), + Level::Trace => "TRACE".bold().purple(), + }; + let module = record + .module_path() + .map(|path| path.split("::").last().unwrap_or(path)) + .unwrap_or("unknown") + .dimmed(); + let elapsed = start.elapsed().as_secs_f32(); + let elapsed_display = format!("T+{elapsed:0.09}").dimmed(); + let display = format!( + "[{elapsed_display}] {level_display:5} {module:^6} {args}", + args = record.args() + ); + + writeln!(buf, "{display}") + }); + + if let Some(path) = log_file { + let log_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .expect("Failed to open log file"); + + logger.target(Target::Pipe(Box::new(log_file))); + } + + if let Some(level) = log_level { + logger.filter_level(level).init(); + } else { + logger.parse_env("DUST_LOG").init(); + } + + let input_bytes = if let Some(source_input) = input.input { + source_input + } else if let Some(path) = &input.source_file { + let mut source_file = File::open(path).expect("Failed to open source file"); + let file_length = source_file + .metadata() + .expect("Failed to read file metadata") + .len(); + let mut buffer = Vec::with_capacity(file_length as usize); + + source_file + .read_to_end(&mut buffer) + .expect("Failed to read source file"); + + buffer + } else { + let mut buffer = Vec::new(); + + stdin() + .read_to_end(&mut buffer) + .expect("Failed to read from stdin"); + + buffer + }; + + match command { + Command::Format { + color, + indent, + line_numbers, + } => { + let source = String::from_utf8(input_bytes).expect("Failed to parse input as UTF-8"); + let formatted = match format(&source, color, indent, line_numbers) { + Ok(formatted) => formatted, + Err(dust_error) => { + let report = dust_error.report(); + + return Err(report); + } + }; + + if let Some(path) = output.output_file { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .expect("Failed to open output file"); + + file.write_all(formatted.as_bytes()) + .expect("Failed to write to output file"); + } else { + println!("{}", formatted); + } + + return Ok(()); + } + Command::Tokenize { style } => { + let source = String::from_utf8(input_bytes).expect("Failed to parse input as UTF-8"); + let tokens = match lex(&source) { + Ok(tokens) => tokens, + Err(dust_error) => { + let report = dust_error.report(); + + return Err(report); + } + }; + + if let Some(path) = output.output_file { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .expect("Failed to open output file"); + + display_token_list(&tokens, style, &mut file) + } else { + display_token_list(&tokens, style, &mut stdout()) + } + + return Ok(()); + } + _ => {} + } + + let chunk = match input.read_format { + IoFormat::Source => { + let source = String::from_utf8(input_bytes).expect("Failed to parse input as UTF-8"); + + match compile(&source) { + Ok(chunk) => chunk, + Err(dust_error) => { + let report = dust_error.report(); + + return Err(report); + } + } + } + IoFormat::Json => { + serde_json::from_slice(&input_bytes).expect("Failed to deserialize chunk from JSON") + } + IoFormat::Postcard => { + postcard::from_bytes(&input_bytes).expect("Failed to deserialize chunk from Postcard") + } + }; + + match command { + Command::Run => match run_chunk(&chunk) { + Ok(Some(value)) => { + println!("{}", value); + + return Ok(()); + } + Ok(None) => return Ok(()), + Err(dust_error) => { + let report = dust_error.report(); + + return Err(report); + } + }, + Command::Disassemble { style } => { + let disassembly = chunk.disassembler().style(style).disassemble(); + + println!("{disassembly}"); + + return Ok(()); + } + _ => {} + } + + let output_bytes = match output.write_format { + IoFormat::Source => { + return Err("Invalid options, cannot compile chunk as source code.".to_string()) + } + IoFormat::Json => serde_json::to_vec(&chunk).expect("Failed to serialize chunk as JSON"), + IoFormat::Postcard => { + let length = postcard::experimental::serialized_size(&chunk) + .expect("Failed to calculate Postcard size"); + let mut buffer = vec![0_u8; length as usize]; + + postcard::to_slice(&chunk, &mut buffer).expect("Failed to serialize chunk as Postcard"); + + buffer + } + }; + + if let Some(path) = output.output_file { + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .expect("Failed to open output file"); + + file.write_all(&output_bytes) + .expect("Failed to write to output file"); + } else { + stdout() + .write_all(&output_bytes) + .expect("Failed to write to stdout"); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/dust-lang/src/chunk.rs b/dust-lang/src/chunk.rs index c75fd2f..5767340 100644 --- a/dust-lang/src/chunk.rs +++ b/dust-lang/src/chunk.rs @@ -5,6 +5,7 @@ //! belong to a named function. use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; @@ -181,21 +182,21 @@ impl Chunk { impl Display for Chunk { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disassembler = self.disassembler().styled(false); + let disassembly = self.disassembler().style(true).disassemble(); - write!(f, "{}", disassembler.disassemble()) + write!(f, "{disassembly}") } } impl Debug for Chunk { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disassembly = self.disassembler().styled(false).disassemble(); + let disassembly = self.disassembler().style(false).disassemble(); if cfg!(debug_assertions) { - write!(f, "\n{}", disassembly) - } else { - write!(f, "{}", disassembly) + write!(f, "\n",)?; } + + write!(f, "{}", disassembly) } } @@ -237,6 +238,12 @@ impl Local { } } +impl Hash for Local { + fn hash(&self, state: &mut H) { + self.identifier_index.hash(state); + } +} + /// Errors that can occur when using a [`Chunk`]. #[derive(Clone, Debug, PartialEq)] pub enum ChunkError { diff --git a/dust-lang/src/compiler.rs b/dust-lang/src/compiler.rs index d43459a..40e088a 100644 --- a/dust-lang/src/compiler.rs +++ b/dust-lang/src/compiler.rs @@ -4,6 +4,7 @@ //! - [`compile`], which compiles the entire input and returns a chunk //! - [`Compiler`], which compiles the input a token at a time while assembling a chunk use std::{ + collections::HashMap, fmt::{self, Display, Formatter}, mem::replace, num::{ParseFloatError, ParseIntError}, @@ -44,19 +45,18 @@ pub fn compile(source: &str) -> Result { /// Low-level tool for compiling the input a token at a time while assembling a chunk. /// /// See the [`compile`] function an example of how to create and use a Compiler. -#[derive(Debug, Eq, PartialEq, PartialOrd)] +#[derive(Debug, Eq, PartialEq)] pub struct Compiler<'src> { chunk: Chunk, lexer: Lexer<'src>, + local_declarations: HashMap, current_token: Token<'src>, current_position: Span, - previous_token: Token<'src>, previous_position: Span, return_type: Option, - local_definitions: Vec, optimization_count: usize, previous_expression_type: Type, minimum_register: u8, @@ -79,12 +79,12 @@ impl<'src> Compiler<'src> { Ok(Compiler { chunk, lexer, + local_declarations: HashMap::new(), current_token, current_position, previous_token: Token::Eof, previous_position: Span(0, 0), return_type: None, - local_definitions: Vec::new(), optimization_count: 0, previous_expression_type: Type::None, minimum_register: 0, @@ -113,21 +113,20 @@ impl<'src> Compiler<'src> { matches!(self.current_token, Token::Eof) } - fn next_register(&mut self) -> u8 { + fn next_register(&self) -> u8 { self.chunk .instructions() .iter() - .rev() - .find_map(|(instruction, _)| { + .filter_map(|(instruction, _)| { if instruction.yields_value() { - let previous = instruction.a(); - let next = previous.overflowing_add(1).0; + let to_register = instruction.a(); - Some(next) + Some(to_register + 1) } else { None } }) + .max() .unwrap_or(self.minimum_register) } @@ -196,7 +195,7 @@ impl<'src> Compiler<'src> { scope: Scope, register_index: u8, ) -> (u8, u8) { - log::debug!("Declare local {identifier}"); + log::info!("Declaring local {identifier}"); let identifier = ConcreteValue::string(identifier); let identifier_index = self.chunk.push_or_get_constant(identifier); @@ -205,11 +204,35 @@ impl<'src> Compiler<'src> { self.chunk .locals_mut() .push(Local::new(identifier_index, r#type, is_mutable, scope)); - self.local_definitions.push(register_index); + self.local_declarations.insert(local_index, register_index); (local_index, identifier_index) } + fn redeclare_local(&mut self, local_index: u8, register_index: u8) -> Result<(), CompileError> { + let local = self.get_local(local_index)?; + + if !self.current_scope.contains(&local.scope) { + let identifier = self + .chunk + .constants() + .get(local.identifier_index as usize) + .map(|constant| constant.to_string()) + .unwrap_or("unknown".to_string()); + + return Err(CompileError::VariableOutOfScope { + identifier, + variable_scope: local.scope, + access_scope: self.current_scope, + position: self.current_position, + }); + } + + self.local_declarations.insert(local_index, register_index); + + Ok(()) + } + fn allow(&mut self, allowed: Token) -> Result { if self.current_token == allowed { self.advance()?; @@ -673,22 +696,19 @@ impl<'src> Compiler<'src> { let local = self.get_local(local_index)?; is_mutable_local = local.is_mutable; - *self - .local_definitions - .get(local_index as usize) - .ok_or_else(|| { - let identifier = self - .chunk - .constants() - .get(local.identifier_index as usize) - .unwrap() - .to_string(); + *self.local_declarations.get(&local_index).ok_or_else(|| { + let identifier = self + .chunk + .constants() + .get(local.identifier_index as usize) + .unwrap() + .to_string(); - CompileError::UndeclaredVariable { - identifier, - position: self.current_position, - } - })? + CompileError::UndeclaredVariable { + identifier, + position: self.current_position, + } + })? } Operation::LoadConstant => { is_constant = true; @@ -733,17 +753,21 @@ impl<'src> Compiler<'src> { let operator = self.current_token; let operator_position = self.current_position; let rule = ParseRule::from(&operator); + let is_assignment = + if let Token::PlusEqual | Token::MinusEqual | Token::StarEqual | Token::SlashEqual = + operator + { + if !left_is_mutable_local { + return Err(CompileError::ExpectedMutableVariable { + found: self.previous_token.to_owned(), + position: left_position, + }); + } - if let Token::PlusEqual | Token::MinusEqual | Token::StarEqual | Token::SlashEqual = - operator - { - if !left_is_mutable_local { - return Err(CompileError::ExpectedMutableVariable { - found: self.previous_token.to_owned(), - position: left_position, - }); - } - } + true + } else { + false + }; self.advance()?; self.parse_sub_expression(&rule.precedence)?; @@ -756,7 +780,7 @@ impl<'src> Compiler<'src> { self.emit_instruction(right_instruction, right_position); } - let register = if left_is_mutable_local { + let register = if is_assignment { left } else { self.next_register() @@ -818,7 +842,7 @@ impl<'src> Compiler<'src> { } fn parse_comparison_binary(&mut self) -> Result<(), CompileError> { - if let Some([Operation::Equal | Operation::Less | Operation::LessEqual, _, _, _]) = + if let Some([Operation::Equal | Operation::Less | Operation::LessEqual, _, _]) = self.get_last_operations() { return Err(CompileError::CannotChainComparison { @@ -911,15 +935,23 @@ impl<'src> Compiler<'src> { } fn parse_logical_binary(&mut self) -> Result<(), CompileError> { - let start_length = self.chunk.len(); let (left_instruction, left_position) = self.pop_last_instruction()?; + + if !left_instruction.yields_value() { + return Err(CompileError::ExpectedExpression { + found: self.previous_token.to_owned(), + position: self.previous_position, + }); + } + + let (_, is_constant, _, _) = self.handle_binary_argument(&left_instruction)?; let operator = self.current_token; let operator_position = self.current_position; let rule = ParseRule::from(&operator); - - let test_instruction = match operator { - Token::DoubleAmpersand => Instruction::test(left_instruction.a(), false), - Token::DoublePipe => Instruction::test(left_instruction.a(), true), + let test_register = left_instruction.a(); + let mut test_instruction = match operator { + Token::DoubleAmpersand => Instruction::test(test_register, true), + Token::DoublePipe => Instruction::test(test_register, false), _ => { return Err(CompileError::ExpectedTokenMultiple { expected: &[TokenKind::DoubleAmpersand, TokenKind::DoublePipe], @@ -929,13 +961,14 @@ impl<'src> Compiler<'src> { } }; + if is_constant { + test_instruction.set_b_is_constant(); + } + self.advance()?; self.emit_instruction(left_instruction, left_position); self.emit_instruction(test_instruction, operator_position); - - let jump_distance = (self.chunk.len() - start_length) as u8; - - self.emit_instruction(Instruction::jump(jump_distance, true), operator_position); + self.emit_instruction(Instruction::jump(1, true), operator_position); self.parse_sub_expression(&rule.precedence)?; self.previous_expression_type = Type::Boolean; @@ -975,17 +1008,14 @@ impl<'src> Compiler<'src> { }); }; - let (is_mutable, local_scope) = { - let local = self.get_local(local_index)?; + let local = self.get_local(local_index)?; + let is_mutable = local.is_mutable; - (local.is_mutable, local.scope) - }; - - if !self.current_scope.contains(&local_scope) { + if !self.current_scope.contains(&local.scope) { return Err(CompileError::VariableOutOfScope { identifier: self.chunk.get_identifier(local_index).unwrap(), position: start_position, - variable_scope: local_scope, + variable_scope: local.scope, access_scope: self.current_scope, }); } @@ -998,10 +1028,10 @@ impl<'src> Compiler<'src> { }); } - self.parse_expression()?; - let register = self.next_register() - 1; + self.parse_expression()?; + self.redeclare_local(local_index, register)?; self.emit_instruction( Instruction::set_local(register, local_index), start_position, @@ -1055,15 +1085,16 @@ impl<'src> Compiler<'src> { fn parse_block(&mut self) -> Result<(), CompileError> { self.advance()?; - self.block_index += 1; + let starting_block = self.current_scope.block_index; + self.block_index += 1; self.current_scope.begin(self.block_index); while !self.allow(Token::RightBrace)? && !self.is_eof() { self.parse(Precedence::None)?; } - self.current_scope.end(); + self.current_scope.end(starting_block); Ok(()) } @@ -1135,6 +1166,13 @@ impl<'src> Compiler<'src> { self.chunk.instructions_mut().pop(); self.chunk.instructions_mut().pop(); self.chunk.instructions_mut().pop(); + } else if let Some((instruction, _)) = self.chunk.instructions().last() { + let test_register = instruction.a(); + + self.emit_instruction( + Instruction::test(test_register, true), + self.current_position, + ) } let if_block_start = self.chunk.len(); @@ -1229,9 +1267,8 @@ impl<'src> Compiler<'src> { if self.chunk.len() >= 4 { let mut optimizer = Optimizer::new(&mut self.chunk); - let optimized = optimizer.optimize_comparison(); - if optimized { + if optimizer.optimize_control_flow() { self.optimization_count += 1 } } @@ -1449,7 +1486,7 @@ impl<'src> Compiler<'src> { fn parse_function(&mut self) -> Result<(), CompileError> { let function_start = self.current_position.0; let mut function_compiler = Compiler::new(self.lexer)?; - let identifier = if let Token::Identifier(text) = function_compiler.current_token { + let identifier_info = if let Token::Identifier(text) = function_compiler.current_token { let position = function_compiler.current_position; function_compiler.advance()?; @@ -1544,7 +1581,7 @@ impl<'src> Compiler<'src> { self.lexer.skip_to(function_end); - if let Some((identifier, identifier_position)) = identifier { + if let Some((identifier, position)) = identifier_info { let (local_index, _) = self.declare_local( identifier, Type::Function(function_type), @@ -1559,7 +1596,7 @@ impl<'src> Compiler<'src> { ); self.emit_instruction( Instruction::define_local(register, local_index, false), - identifier_position, + position, ); self.previous_expression_type = Type::None; diff --git a/dust-lang/src/disassembler.rs b/dust-lang/src/disassembler.rs index 528b23d..238a202 100644 --- a/dust-lang/src/disassembler.rs +++ b/dust-lang/src/disassembler.rs @@ -50,22 +50,22 @@ use crate::{value::ConcreteValue, Chunk, Local}; const INSTRUCTION_HEADER: [&str; 4] = [ "Instructions", "------------", - " i BYTECODE OPERATION INFO POSITION ", - "--- -------- ------------- ------------------------- ----------", + " i POSITION BYTECODE OPERATION INFO ", + "--- ---------- -------- ------------- ------------------------------------", ]; const CONSTANT_HEADER: [&str; 4] = [ "Constants", "---------", - " i VALUE ", - "--- ---------------", + " i TYPE VALUE ", + "--- ---------------- -----------------", ]; const LOCAL_HEADER: [&str; 4] = [ "Locals", "------", - " i IDENTIFIER TYPE MUTABLE SCOPE ", - "--- ---------- ---------------- ------- -------", + " i SCOPE MUTABLE TYPE IDENTIFIER ", + "--- ----- ------- ---------------- ----------------", ]; /// Builder that constructs a human-readable representation of a chunk. @@ -77,7 +77,7 @@ pub struct Disassembler<'a> { source: Option<&'a str>, // Options - styled: bool, + style: bool, indent: usize, } @@ -87,33 +87,26 @@ impl<'a> Disassembler<'a> { output: String::new(), chunk, source: None, - styled: false, + style: false, indent: 0, } } - /// The default width of the disassembly output. To correctly align the output, this should - /// return the width of the longest line that the disassembler is guaranteed to produce. + /// The default width of the disassembly output, including borders. pub fn default_width() -> usize { let longest_line = INSTRUCTION_HEADER[3]; - longest_line.chars().count().max(80) + (longest_line.chars().count() + 2).max(80) } - pub fn source(mut self, source: &'a str) -> Self { + pub fn set_source(&mut self, source: &'a str) -> &mut Self { self.source = Some(source); self } - pub fn styled(mut self, styled: bool) -> Self { - self.styled = styled; - - self - } - - pub fn indent(mut self, indent: usize) -> Self { - self.indent = indent; + pub fn style(mut self, styled: bool) -> Self { + self.style = styled; self } @@ -141,21 +134,16 @@ impl<'a> Disassembler<'a> { (0, extra_space) } }; - let content = if style_bold { - line_characters - .iter() - .collect::() - .bold() - .to_string() - } else if style_dim { - line_characters - .iter() - .collect::() - .dimmed() - .to_string() - } else { - line_characters.iter().collect::() - }; + let mut content = line_characters.iter().collect::(); + + if style_bold { + content = content.bold().to_string(); + } + + if style_dim { + content = content.dimmed().to_string(); + } + let length_before_content = self.output.chars().count(); for _ in 0..self.indent { @@ -195,8 +183,16 @@ impl<'a> Disassembler<'a> { } } + fn push_source(&mut self, source: &str) { + self.push(source, true, false, false, true); + } + + fn push_chunk_info(&mut self, info: &str) { + self.push(info, true, false, true, true); + } + fn push_header(&mut self, header: &str) { - self.push(header, true, self.styled, false, true); + self.push(header, true, self.style, false, true); } fn push_details(&mut self, details: &str) { @@ -240,12 +236,7 @@ impl<'a> Disassembler<'a> { if let Some(source) = self.source { self.push_empty(); - self.push_details( - &source - .replace(" ", "") - .replace("\n\n", " ") - .replace('\n', " "), - ); + self.push_source(&source.split_whitespace().collect::>().join(" ")); self.push_empty(); } @@ -257,7 +248,7 @@ impl<'a> Disassembler<'a> { self.chunk.r#type().return_type ); - self.push(&info_line, true, false, true, true); + self.push_chunk_info(&info_line); self.push_empty(); for line in INSTRUCTION_HEADER { @@ -265,13 +256,13 @@ impl<'a> Disassembler<'a> { } for (index, (instruction, position)) in self.chunk.instructions().iter().enumerate() { + let position = position.to_string(); let bytecode = format!("{:02X}", u32::from(instruction)); let operation = instruction.operation().to_string(); let info = instruction.disassembly_info(self.chunk); - let position = position.to_string(); let instruction_display = - format!("{index:^3} {bytecode:>8} {operation:13} {info:<25} {position:10}"); + format!("{index:^3} {position:^10} {bytecode:>8} {operation:13} {info:^36}"); self.push_details(&instruction_display); } @@ -300,7 +291,7 @@ impl<'a> Disassembler<'a> { .unwrap_or_else(|| "unknown".to_string()); let type_display = r#type.to_string(); let local_display = format!( - "{index:^3} {identifier_display:10} {type_display:16} {mutable:7} {scope:7}" + "{index:^3} {scope:5} {mutable:^7} {type_display:^16} {identifier_display:^16}" ); self.push_details(&local_display); @@ -314,35 +305,34 @@ impl<'a> Disassembler<'a> { for (index, value) in self.chunk.constants().iter().enumerate() { if let ConcreteValue::Function(chunk) = value { - let function_disassembly = chunk - .disassembler() - .styled(self.styled) - .indent(self.indent + 1) - .disassemble(); + let mut function_disassembler = chunk.disassembler().style(self.style); + + function_disassembler.indent = self.indent + 1; + + let function_disassembly = function_disassembler.disassemble(); self.output.push_str(&function_disassembly); continue; } + let type_display = value.r#type().to_string(); let value_display = { - let value_string = value.to_string(); + let mut value_string = value.to_string(); if value_string.len() > 15 { - format!("{value_string:.12}...") - } else { - value_string + value_string = format!("{value_string:.12}..."); } + + value_string }; - let constant_display = format!("{index:^3} {value_display:^15}"); + let constant_display = format!("{index:^3} {type_display:^16} {value_display:^17}"); self.push_details(&constant_display); } self.push_border(&bottom_border); - let _ = self.output.trim_end_matches('\n'); - - self.output + self.output.to_string() } } diff --git a/dust-lang/src/formatter.rs b/dust-lang/src/formatter.rs index 1bb5692..5d0c751 100644 --- a/dust-lang/src/formatter.rs +++ b/dust-lang/src/formatter.rs @@ -5,7 +5,12 @@ use colored::{ColoredString, Colorize, CustomColor}; use crate::{CompileError, DustError, LexError, Lexer, Token}; -pub fn format(source: &str, line_numbers: bool, colored: bool) -> Result { +pub fn format( + source: &str, + colored: bool, + indent: usize, + line_numbers: bool, +) -> Result { let lexer = Lexer::new(source); let formatted = Formatter::new(lexer) .line_numbers(line_numbers) diff --git a/dust-lang/src/instruction.rs b/dust-lang/src/instruction.rs index 96c19b3..0ec5410 100644 --- a/dust-lang/src/instruction.rs +++ b/dust-lang/src/instruction.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; -use crate::{Chunk, NativeFunction, Operation, Type}; +use crate::{Chunk, NativeFunction, Operation}; /// An operation and its arguments for the Dust virtual machine. /// @@ -160,10 +160,10 @@ impl Instruction { instruction } - pub fn test(to_register: u8, test_value: bool) -> Instruction { + pub fn test(test_register: u8, test_value: bool) -> Instruction { let mut instruction = Instruction(Operation::Test as u32); - instruction.set_a(to_register); + instruction.set_b(test_register); instruction.set_c_to_boolean(test_value); instruction @@ -375,27 +375,24 @@ impl Instruction { } pub fn yields_value(&self) -> bool { - match self.operation() { + matches!( + self.operation(), Operation::Add - | Operation::Call - | Operation::Divide - | Operation::GetLocal - | Operation::LoadBoolean - | Operation::LoadConstant - | Operation::LoadList - | Operation::LoadSelf - | Operation::Modulo - | Operation::Multiply - | Operation::Negate - | Operation::Not - | Operation::Subtract => true, - Operation::CallNative => { - let native_function = NativeFunction::from(self.b()); - - *native_function.r#type().return_type != Type::None - } - _ => false, - } + | Operation::Call + | Operation::CallNative + | Operation::Divide + | Operation::GetLocal + | Operation::LoadBoolean + | Operation::LoadConstant + | Operation::LoadList + | Operation::LoadSelf + | Operation::Modulo + | Operation::Multiply + | Operation::Negate + | Operation::Not + | Operation::Subtract + | Operation::TestSet + ) } pub fn disassembly_info(&self, chunk: &Chunk) -> String { @@ -428,7 +425,7 @@ impl Instruction { let jump = self.c_as_boolean(); if jump { - format!("R{to_register} = {boolean} && SKIP") + format!("R{to_register} = {boolean} && JUMP +1") } else { format!("R{to_register} = {boolean}") } @@ -439,7 +436,7 @@ impl Instruction { let jump = self.c_as_boolean(); if jump { - format!("R{register_index} = C{constant_index} && SKIP") + format!("R{register_index} = C{constant_index} JUMP +1") } else { format!("R{register_index} = C{constant_index}") } @@ -463,13 +460,8 @@ impl Instruction { Operation::DefineLocal => { let to_register = self.a(); let local_index = self.b(); - let mutable_display = if self.c_as_boolean() { "mut" } else { "" }; - let identifier_display = match chunk.get_identifier(local_index) { - Some(identifier) => identifier.to_string(), - None => "???".to_string(), - }; - format!("R{to_register} = L{local_index} {mutable_display} {identifier_display}") + format!("L{local_index} = R{to_register}") } Operation::GetLocal => { let local_index = self.b(); @@ -478,12 +470,9 @@ impl Instruction { } Operation::SetLocal => { let local_index = self.b(); - let identifier_display = match chunk.get_identifier(local_index) { - Some(identifier) => identifier.to_string(), - None => "???".to_string(), - }; + let register = self.a(); - format!("L{} = R{} {}", local_index, self.a(), identifier_display) + format!("L{local_index} = R{register}") } Operation::Add => { let to_register = self.a(); @@ -516,37 +505,42 @@ impl Instruction { format!("R{to_register} = {first_argument} % {second_argument}",) } Operation::Test => { - let to_register = self.a(); - let test_value = self.c_as_boolean(); - - format!("if R{to_register} != {test_value} {{ SKIP }}") - } - Operation::TestSet => { - let to_register = self.a(); - let argument = format!("R{}", self.b()); + let test_register = if self.b_is_constant() { + format!("C{}", self.b()) + } else { + format!("R{}", self.b()) + }; let test_value = self.c_as_boolean(); let bang = if test_value { "" } else { "!" }; - format!("if {bang}R{to_register} {{ R{to_register} = R{argument} }}",) + format!("if {bang}{test_register} {{ JUMP +1 }}",) + } + Operation::TestSet => { + let to_register = self.a(); + let test_register = self.b(); + let test_value = self.c_as_boolean(); + let bang = if test_value { "" } else { "!" }; + + format!("if {bang}R{test_register} {{ JUMP +1 }} else {{ R{to_register} = R{test_register} }}") } Operation::Equal => { let comparison_symbol = if self.a_as_boolean() { "==" } else { "!=" }; let (first_argument, second_argument) = format_arguments(); - format!("if {first_argument} {comparison_symbol} {second_argument} {{ SKIP }}") + format!("if {first_argument} {comparison_symbol} {second_argument} {{ JUMP +1 }}") } Operation::Less => { let comparison_symbol = if self.a_as_boolean() { "<" } else { ">=" }; let (first_argument, second_argument) = format_arguments(); - format!("if {first_argument} {comparison_symbol} {second_argument} {{ SKIP }}") + format!("if {first_argument} {comparison_symbol} {second_argument} {{ JUMP +1 }}") } Operation::LessEqual => { let comparison_symbol = if self.a_as_boolean() { "<=" } else { ">" }; let (first_argument, second_argument) = format_arguments(); - format!("if {first_argument} {comparison_symbol} {second_argument} {{ SKIP }}") + format!("if {first_argument} {comparison_symbol} {second_argument} {{ JUMP +1 }}") } Operation::Negate => { let to_register = self.a(); @@ -615,8 +609,8 @@ impl Instruction { if argument_count != 0 { let first_argument = to_register.saturating_sub(argument_count); - for (index, register) in (first_argument..to_register).enumerate() { - if index > 0 { + for register in first_argument..to_register { + if register != first_argument { output.push_str(", "); } @@ -788,16 +782,16 @@ mod tests { } #[test] - fn and() { - let instruction = Instruction::test(4, true); + fn test() { + let instruction = Instruction::test(42, true); assert_eq!(instruction.operation(), Operation::Test); - assert_eq!(instruction.a(), 4); + assert_eq!(instruction.b(), 42); assert!(instruction.c_as_boolean()); } #[test] - fn or() { + fn test_set() { let instruction = Instruction::test_set(4, 1, true); assert_eq!(instruction.operation(), Operation::TestSet); diff --git a/dust-lang/src/lib.rs b/dust-lang/src/lib.rs index 9aaee78..76ba50d 100644 --- a/dust-lang/src/lib.rs +++ b/dust-lang/src/lib.rs @@ -28,11 +28,9 @@ pub use crate::operation::Operation; pub use crate::optimizer::Optimizer; pub use crate::r#type::{EnumType, FunctionType, StructType, Type, TypeConflict}; pub use crate::scope::Scope; -pub use crate::token::{output_token_list, Token, TokenKind, TokenOwned}; -pub use crate::value::{ - AbstractValue, ConcreteValue, RangeValue, ValueError, ValueOwned, ValueRef, -}; -pub use crate::vm::{run, Vm, VmError}; +pub use crate::token::{display_token_list, Token, TokenKind, TokenOwned}; +pub use crate::value::{AbstractValue, ConcreteValue, RangeValue, Value, ValueError, ValueRef}; +pub use crate::vm::{run_source, Vm, VmError}; use std::fmt::Display; diff --git a/dust-lang/src/native_function/logic.rs b/dust-lang/src/native_function/logic.rs index 70004c9..b250fc1 100644 --- a/dust-lang/src/native_function/logic.rs +++ b/dust-lang/src/native_function/logic.rs @@ -1,8 +1,8 @@ use std::io::{self, stdout, Write}; -use crate::{ConcreteValue, Instruction, NativeFunctionError, ValueOwned, Vm, VmError}; +use crate::{ConcreteValue, Instruction, NativeFunctionError, Value, Vm, VmError}; -pub fn panic<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { +pub fn panic<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { let argument_count = instruction.c(); let message = if argument_count == 0 { None @@ -14,7 +14,7 @@ pub fn panic<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result(vm: &'a Vm<'a>, instruction: Instruction) -> Result( - vm: &'a Vm<'a>, - instruction: Instruction, -) -> Result, VmError> { +pub fn to_string<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { let argument_count = instruction.c(); if argument_count != 1 { @@ -52,7 +49,7 @@ pub fn to_string<'a>( let mut string = String::new(); for argument_index in 0..argument_count { - let argument = if let Some(value) = vm.open_register_ignore_empty(argument_index)? { + let argument = if let Some(value) = vm.open_register_allow_empty(argument_index)? { value } else { continue; @@ -62,13 +59,10 @@ pub fn to_string<'a>( string.push_str(&argument_string); } - Ok(Some(ValueOwned::Concrete(ConcreteValue::String(string)))) + Ok(Some(Value::Concrete(ConcreteValue::String(string)))) } -pub fn read_line<'a>( - vm: &'a Vm<'a>, - instruction: Instruction, -) -> Result, VmError> { +pub fn read_line<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { let argument_count = instruction.c(); if argument_count != 0 { @@ -87,7 +81,7 @@ pub fn read_line<'a>( Ok(_) => { buffer = buffer.trim_end_matches('\n').to_string(); - Ok(Some(ValueOwned::Concrete(ConcreteValue::String(buffer)))) + Ok(Some(Value::Concrete(ConcreteValue::String(buffer)))) } Err(error) => Err(VmError::NativeFunction(NativeFunctionError::Io { error: error.kind(), @@ -96,7 +90,7 @@ pub fn read_line<'a>( } } -pub fn write<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { +pub fn write<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { let to_register = instruction.a(); let argument_count = instruction.c(); let mut stdout = stdout(); @@ -114,7 +108,7 @@ pub fn write<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result(vm: &'a Vm<'a>, instruction: Instruction) -> Result( - vm: &'a Vm<'a>, - instruction: Instruction, -) -> Result, VmError> { +pub fn write_line<'a>(vm: &'a Vm<'a>, instruction: Instruction) -> Result, VmError> { let to_register = instruction.a(); let argument_count = instruction.c(); let mut stdout = stdout(); @@ -150,7 +141,7 @@ pub fn write_line<'a>( stdout.write(b" ").map_err(map_err)?; } - let argument = if let Some(value) = vm.open_register_ignore_empty(argument_index)? { + let argument = if let Some(value) = vm.open_register_allow_empty(argument_index)? { value } else { continue; diff --git a/dust-lang/src/native_function/mod.rs b/dust-lang/src/native_function/mod.rs index 886d669..f2058f7 100644 --- a/dust-lang/src/native_function/mod.rs +++ b/dust-lang/src/native_function/mod.rs @@ -12,7 +12,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::{AnnotatedError, FunctionType, Instruction, Span, Type, ValueOwned, Vm, VmError}; +use crate::{AnnotatedError, FunctionType, Instruction, Span, Type, Value, Vm, VmError}; macro_rules! define_native_function { ($(($name:ident, $byte:literal, $str:expr, $type:expr, $function:expr)),*) => { @@ -31,7 +31,7 @@ macro_rules! define_native_function { &self, vm: &mut Vm, instruction: Instruction, - ) -> Result, VmError> { + ) -> Result, VmError> { match self { $( NativeFunction::$name => $function(vm, instruction), diff --git a/dust-lang/src/optimizer.rs b/dust-lang/src/optimizer.rs index d30432d..36b7448 100644 --- a/dust-lang/src/optimizer.rs +++ b/dust-lang/src/optimizer.rs @@ -16,11 +16,11 @@ impl<'a> Optimizer<'a> { } } - /// Optimizes a comparison operation. + /// Optimizes a short control flow pattern. /// - /// Comparison 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. + /// Comparison and 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. /// This makes the following two code snippets compile to the same bytecode: /// /// ```dust @@ -32,15 +32,15 @@ impl<'a> Optimizer<'a> { /// ``` /// /// The instructions must be in the following order: - /// - `Operation::Equal | Operation::Less | Operation::LessEqual` + /// - `Operation::Equal` | `Operation::Less` | `Operation::LessEqual` | `Operation::Test` /// - `Operation::Jump` - /// - `Operation::LoadBoolean | Operation::LoadConstant` - /// - `Operation::LoadBoolean | Operation::LoadConstant` - pub fn optimize_comparison(&mut self) -> bool { + /// - `Operation::LoadBoolean` | `Operation::LoadConstant` + /// - `Operation::LoadBoolean` | `Operation::LoadConstant` + pub fn optimize_control_flow(&mut self) -> bool { if !matches!( self.get_operations(), Some([ - Operation::Equal | Operation::Less | Operation::LessEqual, + Operation::Equal | Operation::Less | Operation::LessEqual | Operation::Test, Operation::Jump, Operation::LoadBoolean | Operation::LoadConstant, Operation::LoadBoolean | Operation::LoadConstant, @@ -49,24 +49,20 @@ impl<'a> Optimizer<'a> { return false; } - log::debug!("Optimizing comparison"); + log::debug!("Consolidating registers for control flow optimization"); let instructions = self.instructions_mut(); - let first_loader_register = { - let first_loader = &mut instructions[2].0; + let first_loader = &mut instructions.iter_mut().nth_back(1).unwrap().0; - first_loader.set_c_to_boolean(true); - first_loader.a() - }; + first_loader.set_c_to_boolean(true); - let second_loader = &mut instructions[3].0; + let first_loader_register = first_loader.a(); + let second_loader = &mut instructions.last_mut().unwrap().0; let mut second_loader_new = Instruction::with_operation(second_loader.operation()); second_loader_new.set_a(first_loader_register); second_loader_new.set_b(second_loader.b()); second_loader_new.set_c(second_loader.c()); - second_loader_new.set_b_to_boolean(second_loader.b_is_constant()); - second_loader_new.set_c_to_boolean(second_loader.c_is_constant()); *second_loader = second_loader_new; @@ -88,9 +84,27 @@ impl<'a> Optimizer<'a> { return false; } - self.instructions_mut().pop(); + log::debug!("Condensing math and SetLocal to math instruction"); - log::debug!("Optimizing by removing redundant SetLocal"); + let instructions = self.instructions_mut(); + let set_local = instructions.pop().unwrap().0; + let set_local_register = set_local.a(); + let math_instruction = &mut instructions.last_mut().unwrap().0; + let mut math_instruction_new = Instruction::with_operation(math_instruction.operation()); + + math_instruction_new.set_a(set_local_register); + math_instruction_new.set_b(math_instruction.b()); + math_instruction_new.set_c(math_instruction.c()); + + if math_instruction.b_is_constant() { + math_instruction_new.set_b_is_constant(); + } + + if math_instruction.c_is_constant() { + math_instruction_new.set_c_is_constant(); + } + + *math_instruction = math_instruction_new; true } diff --git a/dust-lang/src/scope.rs b/dust-lang/src/scope.rs index b0e893a..fd9f5c0 100644 --- a/dust-lang/src/scope.rs +++ b/dust-lang/src/scope.rs @@ -36,19 +36,14 @@ impl Scope { #[inline] pub fn begin(&mut self, block_index: u8) { - self.block_index = block_index; self.depth += 1; + self.block_index = block_index; } #[inline] - pub fn end(&mut self) { + pub fn end(&mut self, block_index: u8) { self.depth -= 1; - - if self.depth == 0 { - self.block_index = 0; - } else { - self.block_index -= 1; - } + self.block_index = block_index; } } diff --git a/dust-lang/src/token.rs b/dust-lang/src/token.rs index c39ccac..96d0d7f 100644 --- a/dust-lang/src/token.rs +++ b/dust-lang/src/token.rs @@ -4,24 +4,25 @@ use std::{ io::Write, }; +use colored::Colorize; use serde::{Deserialize, Serialize}; use crate::Span; -pub fn output_token_list(tokens: &[(Token, Span)], writer: &mut W) { - const HEADER: [&str; 2] = [ - "TOKEN KIND POSITION ", - "------------ ---------- ----------", - ]; +pub fn display_token_list(tokens: &[(Token, Span)], styled: bool, writer: &mut W) { + const HEADER: [&str; 2] = [" TOKEN POSITION ", "------------- ----------"]; writeln!(writer, "{}", HEADER[0]).unwrap(); writeln!(writer, "{}", HEADER[1]).unwrap(); for (token, position) in tokens { - let kind = token.kind().to_string(); - let token = token.to_string(); + let token = if styled { + format!("{:^13}", token.to_string().bold()) + } else { + format!("{token:^13}") + }; - writeln!(writer, "{token:<12} {kind:<10} {position}").unwrap(); + writeln!(writer, "{token} {position:<10}").unwrap(); } } diff --git a/dust-lang/src/value/mod.rs b/dust-lang/src/value/mod.rs index 58e2e3f..5378c36 100644 --- a/dust-lang/src/value/mod.rs +++ b/dust-lang/src/value/mod.rs @@ -12,23 +12,23 @@ use std::fmt::{self, Debug, Display, Formatter}; use crate::{Vm, VmError}; #[derive(Clone, Debug, PartialEq, PartialOrd)] -pub enum ValueOwned { +pub enum Value { Abstract(AbstractValue), Concrete(ConcreteValue), } -impl ValueOwned { +impl Value { pub fn to_concrete_owned(&self, vm: &Vm) -> Result { match self { - ValueOwned::Abstract(abstract_value) => abstract_value.to_concrete_owned(vm), - ValueOwned::Concrete(concrete_value) => Ok(concrete_value.clone()), + Value::Abstract(abstract_value) => abstract_value.to_concrete_owned(vm), + Value::Concrete(concrete_value) => Ok(concrete_value.clone()), } } pub fn display(&self, vm: &Vm) -> Result { match self { - ValueOwned::Abstract(abstract_value) => abstract_value.display(vm), - ValueOwned::Concrete(concrete_value) => Ok(concrete_value.to_string()), + Value::Abstract(abstract_value) => abstract_value.display(vm), + Value::Concrete(concrete_value) => Ok(concrete_value.to_string()), } } } diff --git a/dust-lang/src/vm.rs b/dust-lang/src/vm.rs index 5954db1..b67e7ba 100644 --- a/dust-lang/src/vm.rs +++ b/dust-lang/src/vm.rs @@ -8,11 +8,10 @@ use std::{ use crate::{ compile, AbstractValue, AnnotatedError, Chunk, ChunkError, ConcreteValue, DustError, - Instruction, NativeFunction, NativeFunctionError, Operation, Span, ValueError, ValueOwned, - ValueRef, + Instruction, NativeFunction, NativeFunctionError, Operation, Span, Value, ValueError, ValueRef, }; -pub fn run(source: &str) -> Result, DustError> { +pub fn run_source(source: &str) -> Result, DustError> { let chunk = compile(source)?; let mut vm = Vm::new(&chunk, None); @@ -20,25 +19,24 @@ pub fn run(source: &str) -> Result, DustError> { .map_err(|error| DustError::Runtime { error, source }) } -pub fn run_and_display_output(source: &str) { - match run(source) { - Ok(Some(value)) => println!("{}", value), - Ok(None) => {} - Err(error) => eprintln!("{}", error.report()), - } +pub fn run_chunk(chunk: &Chunk) -> Result, DustError> { + let mut vm = Vm::new(chunk, None); + + vm.run() + .map_err(|error| DustError::Runtime { error, source: "" }) } /// Dust virtual machine. /// /// See the [module-level documentation](index.html) for more information. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct Vm<'a> { chunk: &'a Chunk, stack: Vec, parent: Option<&'a Vm<'a>>, + local_definitions: HashMap, ip: usize, - local_definitions: HashMap, last_assigned_register: Option, current_position: Span, } @@ -51,8 +49,8 @@ impl<'a> Vm<'a> { chunk, stack: Vec::new(), parent, - ip: 0, local_definitions: HashMap::new(), + ip: 0, last_assigned_register: None, current_position: Span(0, 0), } @@ -186,6 +184,7 @@ impl<'a> Vm<'a> { )?; let register = Register::Pointer(Pointer::Stack(from_register)); + self.define_local(to_local, from_register)?; self.set_register(local_register, register)?; } Operation::Add => { @@ -239,9 +238,9 @@ impl<'a> Vm<'a> { self.set_register(to_register, Register::ConcreteValue(remainder))?; } Operation::Test => { - let register = instruction.a(); + let test_register = instruction.b(); let test_value = instruction.c_as_boolean(); - let value = self.open_register(register)?; + let value = self.open_register(test_register)?; let boolean = if let ValueRef::Concrete(ConcreteValue::Boolean(boolean)) = value { *boolean @@ -252,11 +251,33 @@ impl<'a> Vm<'a> { }); }; - if boolean != test_value { + if boolean == test_value { self.ip += 1; } } - Operation::TestSet => todo!(), + Operation::TestSet => { + let to_register = instruction.a(); + let test_register = instruction.b(); + let test_value = instruction.c_as_boolean(); + let value = self.open_register(test_register)?; + let boolean = if let ValueRef::Concrete(ConcreteValue::Boolean(boolean)) = value + { + *boolean + } else { + return Err(VmError::ExpectedBoolean { + found: value.to_concrete_owned(self)?, + position: self.current_position, + }); + }; + + if boolean == test_value { + self.ip += 1; + } else { + let register = Register::Pointer(Pointer::Stack(test_register)); + + self.set_register(to_register, register)?; + } + } Operation::Equal => { let compare_to = instruction.a_as_boolean(); let (left, right) = self.get_arguments(instruction)?; @@ -394,10 +415,10 @@ impl<'a> Vm<'a> { let to_register = instruction.a(); let register = match value { - ValueOwned::Abstract(abstract_value) => { + Value::Abstract(abstract_value) => { Register::AbstractValue(abstract_value) } - ValueOwned::Concrete(concrete_value) => { + Value::Concrete(concrete_value) => { Register::ConcreteValue(concrete_value) } }; @@ -485,7 +506,7 @@ impl<'a> Vm<'a> { } } - pub(crate) fn open_register_ignore_empty( + pub(crate) fn open_register_allow_empty( &self, register_index: u8, ) -> Result, VmError> { diff --git a/dust-lang/tests/basic.rs b/dust-lang/tests/basic.rs index 644f81e..8645d3c 100644 --- a/dust-lang/tests/basic.rs +++ b/dust-lang/tests/basic.rs @@ -22,7 +22,7 @@ fn constant() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(42)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(42)))); } #[test] @@ -43,7 +43,7 @@ fn empty() { vec![] )) ); - assert_eq!(run(source), Ok(None)); + assert_eq!(run_source(source), Ok(None)); } #[test] @@ -81,5 +81,5 @@ fn parentheses_precedence() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(9)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(9)))); } diff --git a/dust-lang/tests/comparison.rs b/dust-lang/tests/comparison.rs index 7d57034..4d07519 100644 --- a/dust-lang/tests/comparison.rs +++ b/dust-lang/tests/comparison.rs @@ -30,7 +30,7 @@ fn equal() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); } #[test] @@ -63,7 +63,7 @@ fn greater() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); } #[test] @@ -96,7 +96,7 @@ fn greater_than_or_equal() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); } #[test] @@ -129,7 +129,7 @@ fn less_than() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(true)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(true)))); } #[test] @@ -162,7 +162,7 @@ fn less_than_or_equal() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(true)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(true)))); } #[test] @@ -195,5 +195,5 @@ fn not_equal() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(true)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(true)))); } diff --git a/dust-lang/tests/control_flow.rs b/dust-lang/tests/control_flow/comparison_expressions.rs similarity index 84% rename from dust-lang/tests/control_flow.rs rename to dust-lang/tests/control_flow/comparison_expressions.rs index 654786c..31eb35f 100644 --- a/dust-lang/tests/control_flow.rs +++ b/dust-lang/tests/control_flow/comparison_expressions.rs @@ -243,76 +243,6 @@ fn if_else_complex() { assert_eq!(run(source), Ok(None)); } -// #[test] -// fn if_else_nested() { -// let source = r#" -// if 0 == 1 { -// if 0 == 2 { -// 1; -// } else { -// 2; -// } -// } else { -// if 0 == 3 { -// 3; -// } else { -// 4; -// } -// }"#; - -// assert_eq!( -// parse(source), -// Ok(Chunk::with_data( -// None, -// vec![ -// ( -// *Instruction::equal(true, 0, 1) -// .set_b_is_constant() -// .set_c_is_constant(), -// Span(14, 16) -// ), -// (Instruction::jump(7, true), Span(14, 16)), -// ( -// *Instruction::equal(true, 0, 2) -// .set_b_is_constant() -// .set_c_is_constant(), -// Span(38, 41) -// ), -// (Instruction::jump(3, true), Span(38, 41)), -// (Instruction::load_constant(0, 1, false), Span(61, 62)), -// (Instruction::jump(1, true1), Span(95, 95)), -// ( -// *Instruction::equal(true, 0, 3) -// .set_b_is_constant() -// .set_c_is_constant(), -// Span(77, 79) -// ), -// (Instruction::jump(3, true), Span(77, 79)), -// (Instruction::load_constant(0, 2, false), Span(94, 95)), -// (Instruction::jump(1, true1), Span(95, 95)), -// (Instruction::load_constant(0, 3, false), Span(114, 115)), -// (Instruction::jump(1, true1), Span(95, 95)), -// (Instruction::load_constant(0, 4, false), Span(134, 135)), -// (Instruction::r#return(true), Span(146, 146)), -// ], -// vec![ -// ConcreteValue::integer(0), -// ConcreteValue::integer(1), -// ConcreteValue::integer(0), -// ConcreteValue::integer(2), -// ConcreteValue::integer(1), -// ConcreteValue::integer(0), -// ConcreteValue::integer(3), -// ConcreteValue::integer(3), -// ConcreteValue::integer(4) -// ], -// vec![] -// )) -// ); - -// assert_eq!(run(source), Ok(Some(ConcreteValue::integer(4)))); -// } - #[test] fn if_else_false() { let source = "if 1 == 2 { panic(); 0 } else { 42 }"; diff --git a/dust-lang/tests/control_flow/logic_expressions.rs b/dust-lang/tests/control_flow/logic_expressions.rs new file mode 100644 index 0000000..a00b14c --- /dev/null +++ b/dust-lang/tests/control_flow/logic_expressions.rs @@ -0,0 +1,34 @@ +#[test] +fn if_true() { + let source = "if true && true { 42 } else { 0 }"; + + assert_eq!( + compile(source), + Ok(Chunk::with_data( + None, + FunctionType { + type_parameters: None, + value_parameters: None, + return_type: Box::new(Type::None) + }, + vec![ + ( + *Instruction::equal(true, 0, 0) + .set_b_is_constant() + .set_c_is_constant(), + Span(5, 7) + ), + (Instruction::jump(1, true), Span(10, 11)), + ( + Instruction::call_native(0, NativeFunction::Panic, 0), + Span(12, 19) + ), + (Instruction::r#return(false), Span(21, 21)) + ], + vec![ConcreteValue::Integer(1)], + vec![] + )), + ); + + assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(42))),); +} diff --git a/dust-lang/tests/functions.rs b/dust-lang/tests/functions.rs index 26b193c..34469db 100644 --- a/dust-lang/tests/functions.rs +++ b/dust-lang/tests/functions.rs @@ -5,7 +5,7 @@ fn function() { let source = "fn(a: int, b: int) -> int { a + b }"; assert_eq!( - run(source), + run_source(source), Ok(Some(ConcreteValue::Function(Chunk::with_data( None, FunctionType { @@ -23,8 +23,8 @@ fn function() { ], vec![ConcreteValue::string("a"), ConcreteValue::string("b"),], vec![ - Local::new(0, Type::Integer, false, Scope::default()), - Local::new(1, Type::Integer, false, Scope::default()) + Local::new(0, Type::Integer, false, Scope::default(), 0), + Local::new(1, Type::Integer, false, Scope::default(), 1) ] )))) ); @@ -64,8 +64,8 @@ fn function_call() { ], vec![ConcreteValue::string("a"), ConcreteValue::string("b"),], vec![ - Local::new(0, Type::Integer, false, Scope::default()), - Local::new(1, Type::Integer, false, Scope::default()) + Local::new(0, Type::Integer, false, Scope::default(), 0), + Local::new(1, Type::Integer, false, Scope::default(), 1) ] )), ConcreteValue::Integer(1), @@ -75,7 +75,7 @@ fn function_call() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(3)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(3)))); } #[test] @@ -93,7 +93,6 @@ fn function_declaration() { }, vec![ (Instruction::load_constant(0, 0, false), Span(0, 40)), - (Instruction::define_local(0, 0, false), Span(3, 6)), (Instruction::r#return(false), Span(40, 40)) ], vec![ @@ -110,8 +109,8 @@ fn function_declaration() { ], vec![ConcreteValue::string("a"), ConcreteValue::string("b")], vec![ - Local::new(0, Type::Integer, false, Scope::default()), - Local::new(1, Type::Integer, false, Scope::default()) + Local::new(0, Type::Integer, false, Scope::default(), 0), + Local::new(1, Type::Integer, false, Scope::default(), 1) ] )), ConcreteValue::string("add"), @@ -125,9 +124,10 @@ fn function_declaration() { }), false, Scope::default(), - )], + 0 + ),], )), ); - assert_eq!(run(source), Ok(None)); + assert_eq!(run_source(source), Ok(None)); } diff --git a/dust-lang/tests/lists.rs b/dust-lang/tests/lists.rs index c3aca69..70bf0bf 100644 --- a/dust-lang/tests/lists.rs +++ b/dust-lang/tests/lists.rs @@ -25,7 +25,7 @@ fn empty_list() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::list([])))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::list([])))); } #[test] @@ -61,7 +61,7 @@ fn list() { ); assert_eq!( - run(source), + run_source(source), Ok(Some(ConcreteValue::list([ ConcreteValue::Integer(1), ConcreteValue::Integer(2), @@ -117,7 +117,7 @@ fn list_with_complex_expression() { ); assert_eq!( - run(source), + run_source(source), Ok(Some(ConcreteValue::list([ ConcreteValue::Integer(1), ConcreteValue::Integer(-15) @@ -164,7 +164,7 @@ fn list_with_simple_expression() { ); assert_eq!( - run(source), + run_source(source), Ok(Some(ConcreteValue::list([ ConcreteValue::Integer(1), ConcreteValue::Integer(5), diff --git a/dust-lang/tests/logic.rs b/dust-lang/tests/logic.rs index 0a5431c..a4e3f27 100644 --- a/dust-lang/tests/logic.rs +++ b/dust-lang/tests/logic.rs @@ -4,34 +4,6 @@ use dust_lang::*; fn and() { let source = "true && false"; - assert_eq!( - compile(source), - Ok(Chunk::with_data( - None, - FunctionType { - type_parameters: None, - value_parameters: None, - return_type: Box::new(Type::Boolean), - }, - vec![ - (Instruction::load_boolean(0, true, false), Span(0, 4)), - (Instruction::test(0, false), Span(5, 7)), - (Instruction::jump(1, true), Span(5, 7)), - (Instruction::load_boolean(1, false, false), Span(8, 13)), - (Instruction::r#return(true), Span(13, 13)), - ], - vec![], - vec![] - )) - ); - - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); -} - -#[test] -fn or() { - let source = "true || false"; - assert_eq!( compile(source), Ok(Chunk::with_data( @@ -53,7 +25,35 @@ fn or() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(true)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); +} + +#[test] +fn or() { + let source = "true || false"; + + assert_eq!( + compile(source), + Ok(Chunk::with_data( + None, + FunctionType { + type_parameters: None, + value_parameters: None, + return_type: Box::new(Type::Boolean), + }, + vec![ + (Instruction::load_boolean(0, true, false), Span(0, 4)), + (Instruction::test(0, false), Span(5, 7)), + (Instruction::jump(1, true), Span(5, 7)), + (Instruction::load_boolean(1, false, false), Span(8, 13)), + (Instruction::r#return(true), Span(13, 13)), + ], + vec![], + vec![] + )) + ); + + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(true)))); } #[test] @@ -71,22 +71,20 @@ fn variable_and() { }, vec![ (Instruction::load_boolean(0, true, false), Span(8, 12)), - (Instruction::define_local(0, 0, false), Span(4, 5)), (Instruction::load_boolean(1, false, false), Span(22, 27)), - (Instruction::define_local(1, 1, false), Span(18, 19)), (Instruction::get_local(2, 0), Span(29, 30)), - (Instruction::test(2, false), Span(31, 33)), + (Instruction::test(2, true), Span(31, 33)), (Instruction::jump(1, true), Span(31, 33)), (Instruction::get_local(3, 1), Span(34, 35)), (Instruction::r#return(true), Span(35, 35)), ], vec![ConcreteValue::string("a"), ConcreteValue::string("b"),], vec![ - Local::new(0, Type::Boolean, false, Scope::default()), - Local::new(1, Type::Boolean, false, Scope::default()), + Local::new(0, Type::Boolean, false, Scope::default(), 0), + Local::new(1, Type::Boolean, false, Scope::default(), 1), ] )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); } diff --git a/dust-lang/tests/loops.rs b/dust-lang/tests/loops.rs index 6ae4183..7635586 100644 --- a/dust-lang/tests/loops.rs +++ b/dust-lang/tests/loops.rs @@ -15,7 +15,6 @@ fn r#while() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 13)), - (Instruction::define_local(0, 0, true), Span(8, 9)), ( *Instruction::less(true, 0, 2).set_c_is_constant(), Span(23, 24) @@ -32,9 +31,9 @@ fn r#while() { ConcreteValue::Integer(5), ConcreteValue::Integer(1), ], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(5)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(5)))); } diff --git a/dust-lang/tests/math.rs b/dust-lang/tests/math.rs index 6caa8b7..eabdb47 100644 --- a/dust-lang/tests/math.rs +++ b/dust-lang/tests/math.rs @@ -27,7 +27,7 @@ fn add() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(3)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(3)))); } #[test] @@ -45,7 +45,6 @@ fn add_assign() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 13)), - (Instruction::define_local(0, 0, true), Span(8, 9)), (*Instruction::add(0, 0, 2).set_c_is_constant(), Span(17, 19)), (Instruction::get_local(1, 0), Span(23, 24)), (Instruction::r#return(true), Span(24, 24)) @@ -55,11 +54,11 @@ fn add_assign() { ConcreteValue::string("a"), ConcreteValue::Integer(2) ], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(3)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(3)))); } #[test] @@ -121,7 +120,7 @@ fn divide() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(1)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(1)))); } #[test] @@ -139,7 +138,6 @@ fn divide_assign() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 13)), - (Instruction::define_local(0, 0, true), Span(8, 9)), ( *Instruction::divide(0, 0, 0).set_c_is_constant(), Span(17, 19) @@ -148,11 +146,11 @@ fn divide_assign() { (Instruction::r#return(true), Span(24, 24)) ], vec![ConcreteValue::Integer(2), ConcreteValue::string("a")], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(1)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(1)))); } #[test] @@ -215,7 +213,7 @@ fn math_operator_precedence() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(1)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(1)))); } #[test] @@ -245,7 +243,7 @@ fn multiply() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(2)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(2)))); } #[test] @@ -263,7 +261,6 @@ fn multiply_assign() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 13)), - (Instruction::define_local(0, 0, true), Span(8, 9)), ( *Instruction::multiply(0, 0, 2).set_c_is_constant(), Span(17, 19) @@ -276,11 +273,11 @@ fn multiply_assign() { ConcreteValue::string("a"), ConcreteValue::Integer(3) ], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(6)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(6)))); } #[test] @@ -326,7 +323,7 @@ fn subtract() { )) ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(-1)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(-1)))); } #[test] @@ -344,7 +341,6 @@ fn subtract_assign() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 14)), - (Instruction::define_local(0, 0, true), Span(8, 9)), ( *Instruction::subtract(0, 0, 2).set_c_is_constant(), Span(18, 20) @@ -357,11 +353,11 @@ fn subtract_assign() { ConcreteValue::string("x"), ConcreteValue::Integer(2) ], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(40)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(40)))); } #[test] diff --git a/dust-lang/tests/native_functions.rs b/dust-lang/tests/native_functions.rs index 2725c5b..5df5587 100644 --- a/dust-lang/tests/native_functions.rs +++ b/dust-lang/tests/native_functions.rs @@ -31,7 +31,7 @@ fn panic() { ); assert_eq!( - run(source), + run_source(source), Err(DustError::Runtime { error: VmError::NativeFunction(NativeFunctionError::Panic { message: Some("Goodbye world! 42".to_string()), @@ -68,5 +68,5 @@ fn to_string() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::string("42")))) + assert_eq!(run_source(source), Ok(Some(ConcreteValue::string("42")))) } diff --git a/dust-lang/tests/scopes.rs b/dust-lang/tests/scopes.rs index 46ffb85..69f66dd 100644 --- a/dust-lang/tests/scopes.rs +++ b/dust-lang/tests/scopes.rs @@ -9,7 +9,7 @@ fn allow_access_to_parent_scope() { } "#; - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(1)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(1)))); } #[test] @@ -60,16 +60,16 @@ fn block_scope() { ConcreteValue::string("e"), ], vec![ - Local::new(1, Type::Integer, false, Scope::new(0, 0)), - Local::new(3, Type::Integer, false, Scope::new(1, 1)), - Local::new(5, Type::Integer, false, Scope::new(2, 2)), - Local::new(7, Type::Integer, false, Scope::new(1, 1)), - Local::new(8, Type::Integer, false, Scope::new(0, 0)), + Local::new(1, Type::Integer, false, Scope::new(0, 0), 0), + Local::new(3, Type::Integer, false, Scope::new(1, 1), 0), + Local::new(5, Type::Integer, false, Scope::new(2, 2), 0), + Local::new(7, Type::Integer, false, Scope::new(1, 1), 0), + Local::new(8, Type::Integer, false, Scope::new(0, 0), 0), ] )), ); - assert_eq!(run(source), Ok(None)); + assert_eq!(run_source(source), Ok(None)); } #[test] @@ -81,17 +81,17 @@ fn multiple_block_scopes() { { let c = 1; } - let d = 2; + let d = b; } - let q = 42; + let q = a; { let b = 42; { let c = 1; } - let d = 2; + let d = b; } - let e = 1; + let e = a; "; assert_eq!( @@ -110,19 +110,19 @@ fn multiple_block_scopes() { (Instruction::define_local(1, 1, false), Span(46, 47)), (Instruction::load_constant(2, 4, false), Span(92, 93)), (Instruction::define_local(2, 2, false), Span(88, 89)), - (Instruction::load_constant(3, 6, false), Span(129, 130)), + (Instruction::get_local(3, 1), Span(129, 130)), (Instruction::define_local(3, 3, false), Span(125, 126)), - (Instruction::load_constant(4, 2, false), Span(158, 160)), + (Instruction::get_local(4, 0), Span(158, 159)), (Instruction::define_local(4, 4, false), Span(154, 155)), - (Instruction::load_constant(5, 2, false), Span(192, 194)), - (Instruction::define_local(5, 5, false), Span(188, 189)), - (Instruction::load_constant(6, 4, false), Span(234, 235)), - (Instruction::define_local(6, 6, false), Span(230, 231)), - (Instruction::load_constant(7, 6, false), Span(271, 272)), - (Instruction::define_local(7, 7, false), Span(267, 268)), - (Instruction::load_constant(8, 4, false), Span(300, 301)), - (Instruction::define_local(8, 8, false), Span(296, 297)), - (Instruction::r#return(false), Span(307, 307)) + (Instruction::load_constant(5, 2, false), Span(191, 193)), + (Instruction::define_local(5, 5, false), Span(187, 188)), + (Instruction::load_constant(6, 4, false), Span(233, 234)), + (Instruction::define_local(6, 6, false), Span(229, 230)), + (Instruction::get_local(7, 5), Span(270, 271)), + (Instruction::define_local(7, 7, false), Span(266, 267)), + (Instruction::get_local(8, 0), Span(299, 300)), + (Instruction::define_local(8, 8, false), Span(295, 296)), + (Instruction::r#return(false), Span(306, 306)) ], vec![ ConcreteValue::Integer(0), @@ -131,26 +131,25 @@ fn multiple_block_scopes() { ConcreteValue::string("b"), ConcreteValue::Integer(1), ConcreteValue::string("c"), - ConcreteValue::Integer(2), ConcreteValue::string("d"), ConcreteValue::string("q"), ConcreteValue::string("e"), ], vec![ - Local::new(1, Type::Integer, false, Scope::new(0, 0)), - Local::new(3, Type::Integer, false, Scope::new(1, 1)), - Local::new(5, Type::Integer, false, Scope::new(2, 2)), - Local::new(7, Type::Integer, false, Scope::new(1, 1)), - Local::new(8, Type::Integer, false, Scope::new(0, 0)), - Local::new(3, Type::Integer, false, Scope::new(1, 3)), - Local::new(5, Type::Integer, false, Scope::new(2, 4)), - Local::new(7, Type::Integer, false, Scope::new(1, 3)), - Local::new(9, Type::Integer, false, Scope::new(0, 0)), + Local::new(1, Type::Integer, false, Scope::new(0, 0), 0), + Local::new(3, Type::Integer, false, Scope::new(1, 1), 0), + Local::new(5, Type::Integer, false, Scope::new(2, 2), 0), + Local::new(6, Type::Integer, false, Scope::new(1, 1), 0), + Local::new(7, Type::Integer, false, Scope::new(0, 0), 0), + Local::new(3, Type::Integer, false, Scope::new(1, 3), 0), + Local::new(5, Type::Integer, false, Scope::new(2, 4), 0), + Local::new(6, Type::Integer, false, Scope::new(1, 3), 0), + Local::new(8, Type::Integer, false, Scope::new(0, 0), 0), ] )), ); - assert_eq!(run(source), Ok(None)); + assert_eq!(run_source(source), Ok(None)); } #[test] @@ -163,7 +162,7 @@ fn disallow_access_to_child_scope() { "#; assert_eq!( - run(source), + run_source(source), Err(DustError::Compile { error: CompileError::VariableOutOfScope { identifier: "x".to_string(), @@ -188,7 +187,7 @@ fn disallow_access_to_child_scope_nested() { "#; assert_eq!( - run(source), + run_source(source), Err(DustError::Compile { error: CompileError::VariableOutOfScope { identifier: "x".to_string(), @@ -213,7 +212,7 @@ fn disallow_access_to_sibling_scope() { "#; assert_eq!( - run(source), + run_source(source), Err(DustError::Compile { error: CompileError::VariableOutOfScope { identifier: "x".to_string(), @@ -240,7 +239,7 @@ fn disallow_access_to_sibling_scope_nested() { "#; assert_eq!( - run(source), + run_source(source), Err(DustError::Compile { error: CompileError::VariableOutOfScope { identifier: "x".to_string(), diff --git a/dust-lang/tests/unary_operations.rs b/dust-lang/tests/unary_operations.rs index 18d15c2..e4b6a14 100644 --- a/dust-lang/tests/unary_operations.rs +++ b/dust-lang/tests/unary_operations.rs @@ -22,7 +22,7 @@ fn negate() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(-42)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(-42)))); } #[test] @@ -48,5 +48,5 @@ fn not() { )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Boolean(false)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Boolean(false)))); } diff --git a/dust-lang/tests/variables.rs b/dust-lang/tests/variables.rs index 90f3a34..520f38b 100644 --- a/dust-lang/tests/variables.rs +++ b/dust-lang/tests/variables.rs @@ -15,15 +15,14 @@ fn define_local() { }, vec![ (Instruction::load_constant(0, 0, false), Span(8, 10)), - (Instruction::define_local(0, 0, false), Span(4, 5)), (Instruction::r#return(false), Span(11, 11)) ], vec![ConcreteValue::Integer(42), ConcreteValue::string("x")], - vec![Local::new(1, Type::Integer, false, Scope::default())] + vec![Local::new(1, Type::Integer, false, Scope::default(), 0)] )), ); - assert_eq!(run(source), Ok(None)); + assert_eq!(run_source(source), Ok(None)); } #[test] @@ -58,9 +57,8 @@ fn set_local() { }, vec![ (Instruction::load_constant(0, 0, false), Span(12, 14)), - (Instruction::define_local(0, 0, true), Span(8, 9)), (Instruction::load_constant(1, 2, false), Span(20, 22)), - (Instruction::set_local(1, 0), Span(16, 17)), + (Instruction::set_local(0, 0), Span(16, 17)), (Instruction::get_local(2, 0), Span(24, 25)), (Instruction::r#return(true), Span(25, 25)), ], @@ -69,9 +67,9 @@ fn set_local() { ConcreteValue::string("x"), ConcreteValue::Integer(42) ], - vec![Local::new(1, Type::Integer, true, Scope::default())] + vec![Local::new(1, Type::Integer, true, Scope::default(), 0)] )), ); - assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(42)))); + assert_eq!(run_source(source), Ok(Some(ConcreteValue::Integer(42)))); } diff --git a/dust-shell/src/main.rs b/dust-shell/src/main.rs deleted file mode 100644 index d8f4cd6..0000000 --- a/dust-shell/src/main.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{ - fs::read_to_string, - io::{stdout, Write}, -}; - -use clap::Parser; -use colored::Colorize; -use dust_lang::{compile, format, lex, output_token_list, run}; -use log::{Level, LevelFilter}; - -#[derive(Parser)] -struct Cli { - /// Source code sent via command line - #[arg(short, long)] - command: Option, - - /// Whether to output formatted source code instead of running the program - #[arg(short, long)] - format: bool, - - /// Whether to output line numbers in formatted source code - #[arg(long)] - format_line_numbers: Option, - - /// Whether to output colors in formatted source code - #[arg(long)] - format_colored: Option, - - /// Whether to output the disassembled chunk instead of running the program - #[arg(short, long)] - parse: bool, - - /// Whether to style the disassembled chunk - #[arg(long)] - style_disassembly: Option, - - /// Whether to tokenize the source code instead of running the program - #[arg(short, long)] - tokenize: bool, - - /// Log level - #[arg(short, long)] - log: Option, - - /// Path to a source code file - path: Option, -} - -fn main() { - let args = Cli::parse(); - let mut logger = env_logger::builder(); - - logger.format(|buf, record| { - let level_display = match record.level() { - Level::Info => "INFO".bold().white(), - Level::Debug => "DEBUG".bold().blue(), - Level::Warn => "WARN".bold().yellow(), - Level::Error => "ERROR".bold().red(), - Level::Trace => "TRACE".bold().purple(), - }; - let module = record - .module_path() - .map(|path| path.split("::").last().unwrap_or(path)) - .unwrap_or("unknown") - .dimmed(); - let display = format!("{level_display:5} {module:^6} {args}", args = record.args()); - - writeln!(buf, "{display}") - }); - - if let Some(level) = args.log { - logger.filter_level(level).init(); - } else { - logger.parse_env("DUST_LOG").init(); - } - - let source = if let Some(path) = &args.path { - &read_to_string(path).expect("Failed to read file") - } else if let Some(command) = &args.command { - command - } else { - eprintln!("No input provided"); - - return; - }; - - if args.format { - log::info!("Formatting source"); - - let line_numbers = args.format_line_numbers.unwrap_or(true); - let colored = args.format_colored.unwrap_or(true); - - match format(source, line_numbers, colored) { - Ok(formatted) => println!("{}", formatted), - Err(error) => { - eprintln!("{}", error.report()); - } - } - } - - if args.tokenize { - log::info!("Tokenizing source"); - - match lex(source) { - Ok(tokens) => output_token_list(&tokens, &mut stdout()), - Err(error) => eprintln!("{}", error.report()), - } - } - - if args.parse { - log::info!("Parsing source"); - - let styled = args.style_disassembly.unwrap_or(true); - - match compile(source) { - Ok(chunk) => { - let disassembly = chunk - .disassembler() - .source(source) - .styled(styled) - .disassemble(); - - println!("{}", disassembly); - } - Err(error) => { - eprintln!("{}", error.report()); - } - } - } - - if args.format || args.tokenize || args.parse { - return; - } - - match run(source) { - Ok(Some(value)) => println!("{}", value), - Ok(None) => {} - Err(error) => { - eprintln!("{}", error.report()); - } - } -} - -#[cfg(test)] -mod tests { - use clap::CommandFactory; - - use super::*; - - #[test] - fn verify_cli() { - Cli::command().debug_assert(); - } -} diff --git a/examples/assets/fibonacci.py b/examples/assets/fibonacci.py new file mode 100644 index 0000000..c4c596f --- /dev/null +++ b/examples/assets/fibonacci.py @@ -0,0 +1,11 @@ +def fib(n): + if n == 0: + return 0 + elif n == 1: + return 1 + else: + return fib(n-1) + fib(n-2) + + +# Driver Program +print(fib(25)) diff --git a/examples/fizzbuzz.ds b/examples/fizzbuzz.ds index 87a4bbf..d69fc8e 100644 --- a/examples/fizzbuzz.ds +++ b/examples/fizzbuzz.ds @@ -4,17 +4,22 @@ while count <= 15 { let divides_by_3 = count % 3 == 0 let divides_by_5 = count % 5 == 0 - let output = if divides_by_3 && divides_by_5 { - "fizzbuzz" - } else if divides_by_3 { - "fizz" - } else if divides_by_5 { - "buzz" - } else { - to_string(count) + if divides_by_3 && divides_by_5 { + write_line("fizzbuzz") + return } - write_line(output) + if divides_by_3 { + write_line("fizz") + return + } + + if divides_by_5 { + write_line("buzz") + return + } + + write_line(count) count += 1 }