1
0

Edit README; Fix a test; Try tracing the compiler and VM

This commit is contained in:
Jeff 2024-12-26 14:29:04 -05:00
parent dc3cc13b12
commit 950c36d601
5 changed files with 72 additions and 154 deletions

172
README.md
View File

@ -48,16 +48,6 @@ fn fib (n: int) -> int {
write_line(fib(25)) write_line(fib(25))
``` ```
Dust is an interpreted language that is compiled to bytecode by a hand-written lexer and parser. It
uses a custom multi-threaded register-based virtual machine with separate-thread garbage collection.
Competing with the runtime performance of Rust or C++ *is not* a goal. But competing with the
approachability and simplicity of those languages *is* a goal. Dust *does* intend to be faster than
Python, Ruby and NodeJS while also offering a superior development experience and more reliable code
due to its static typing. Dust's development approach is informed by some books[^1] and academic
research[^4] as well as practical insight from papers[^2] written by language authors. See the
[Inspiration](README#Inspiration) section for more information or keep reading to learn about Dust's
features.
## Goals ## Goals
This project's goal is to deliver a language that not only *works* but that offers genunine value This project's goal is to deliver a language that not only *works* but that offers genunine value
@ -94,56 +84,6 @@ first sentence, Dust's general aspirations are to be **fast**, **safe** and **ea
- **Low Resource Usage** Despite its performance, Dust's use of memory and CPU power should be - **Low Resource Usage** Despite its performance, Dust's use of memory and CPU power should be
conservative and predictable enough to accomodate a wide range of devices. conservative and predictable enough to accomodate a wide range of devices.
These are the project's general design goals. There are many more implementation goals. Among them
are:
- Effortless Concurrency: Dust should offer an excellent experience for writing multi-threaded
programs. The language's native functions should offer an API for spawning threads, sending
messages and waiting for results. When using these features, Dust should be much faster than any
single-threaded language. However, Dust should be fast even when running on a single thread.
Single-threaded performce is the best predictor of multi-threaded performance so continuing to
optimize how each thread executes instructions, accesses memory and moves pointers is the best
way to ensure that Dust is fast in all scenarios.
- Embeddability: The library should be easy to use so that Dust can be built into other
applications. Dust should compile to WebAssembly and offer examples of how to use it in a web
application. The user should be able to query the VM for information about the program's state
and control the program's execution. It should be possible to view and modify the value of a
variable and inspect the call stack.
- Data Fluency: Dust's value type should support conversion to and from arbitrary data in formats
like JSON, YAML, TOML and CSV. Pulling data into a Dust program should be easy, with built-in
functions offering conversion for the most widely used formats.
- Portability: Dust should run on as many architectures and operating systems as possible. Using
fewer dependencies and avoiding platform-specific code will help Dust achieve this goal. The
Dust library should be available as a WebAssembly module.
- Developer Experience: Dust should be fun and easy to use. That implies easy installation and the
availability of tutorials and how-to guides. The CLI should be predictable and feature-rich,
with features that make it easy to write and debug Dust code like formatting, bytecode
disassembly and logging.
- Advanced Type System: Dust should implement composite types, aliases and generics. The type
system should use a descriptive syntax that is easy to understand. Dust's type system should be
static, meaning that types are checked before a program reaches the VM. Dust is not a
graduallly typed language, its VM is and should remain type-agnostic.
- Thorough Testing: Primarily, the output of Dust's compiler and VM should be tested with programs
that cover all of the language's features. The tests should be actively maintained and should be
changed frequently to reflect a growing project that is constantly discovering new optimizations
and opportunities for improvement.
## Project Status
This project is maintained by a single developer. For now, its primary home is on a private git
server. The GitHub mirror is updated automatically and should carry the latest branches. There are
no other contributors at this time but the project is open to feedback and should eventually accept
contributions.
For now, both the library API and the implementation details are freely changed and the CLI has not
been published. Dust is both an ambitious project and a continuous experiment in language design.
Features may be redesigned and reimplemented at will when they do not meet the project's performance
or usability goals. This approach maximizes the development experience as a learning opportunity and
enforces a high standard of quality but slows down the process of delivering features to users.
Eventually, Dust will reach a stable release and will be ready for general use. As the project
approaches this milestone, the experimental nature of the project will be reduced and a replaced
with a focus on stability and improvement.
## Language Overview ## Language Overview
This is a quick overview of Dust's syntax features. It skips over the aspects that are familiar to This is a quick overview of Dust's syntax features. It skips over the aspects that are familiar to
@ -167,57 +107,6 @@ So while the syntax is by no means compatible, it is superficially similar, even
syntax highlighting for Rust code works well with Dust code. This is not a design goal but a happy syntax highlighting for Rust code works well with Dust code. This is not a design goal but a happy
coincidence. coincidence.
### Semicolons
Dust borrowed Rust's approach to semicolons and their effect on evaluation and relaxed the rules to
accomated different styles of coding. Rust, isn't designed for command lines or REPLs but Dust could
be well-suited to those applications. Dust needs to work in a source file or in an ad-hoc one-liner
sent to the CLI. Thus, semicolons are optional in most cases.
There are two things you need to know about semicolons in Dust:
- Semicolons suppress the value of whatever they follow. The preceding statement or expression will
have the type `none` and will not evaluate to a value.
- If a semicolon does not change how the program runs, it is optional.
This example shows three statements with semicolons. The compiler knows that a `let` statement
cannot produce a value and will always have the type `none`. Thanks to static typing, it also knows
that the `write_line` function has no return value so the function call also has the type `none`.
Therefore, these semicolons are optional.
```rust
let a = 40;
let b = 2;
write_line("The answer is ", a + b);
```
Removing the semicolons does not alter the execution pattern or the return value.
```rust
let x = 10
let y = 3
write_line("The remainder is ", x % y)
```
The next example produces a compiler error because the `if` block returns a value of type `int` but
the `else` block does not return a value at all. Dust does not allow branches of the same `if/else`
statement to have different types. In this case, adding a semicolon after the `777` expression fixes
the error by supressing the value.
```rust
// !!! Compile Error !!!
let input = read_line()
let reward = if input == "42" {
write_line("You got it! Here's your reward.")
777 // <- We need a semicolon here
} else {
write_line(input, " is not the answer.")
}
```
### Statements and Expressions ### Statements and Expressions
Dust is composed of statements and expressions. If a statement ends in an expression without a Dust is composed of statements and expressions. If a statement ends in an expression without a
@ -270,13 +159,54 @@ model is more accomodating without sacrificing expressiveness. In Rust, semicolo
and *meaningful*, which provides excellent consistency but lacks flexibility. In JavaScript, and *meaningful*, which provides excellent consistency but lacks flexibility. In JavaScript,
semicolons are *required* and *meaningless*, which is a source of confusion for many developers. semicolons are *required* and *meaningless*, which is a source of confusion for many developers.
### Control Flow Dust borrowed Rust's approach to semicolons and their effect on evaluation and relaxed the rules to
accomated different styles of coding. Rust isn't designed for command lines or REPLs but Dust is
well-suited to those applications. Dust needs to work in a source file or in an ad-hoc one-liner
sent to the CLI. Thus, semicolons are optional in most cases.
-- TODO -- There are two things you need to know about semicolons in Dust:
### Functions - Semicolons suppress the value of whatever they follow. The preceding statement or expression will
have the type `none` and will not evaluate to a value.
- If a semicolon does not change how the program runs, it is optional.
-- TODO -- This example shows three statements with semicolons. The compiler knows that a `let` statement
cannot produce a value and will always have the type `none`. Thanks to static typing, it also knows
that the `write_line` function has no return value so the function call also has the type `none`.
Therefore, these semicolons are optional.
```rust
let a = 40;
let b = 2;
write_line("The answer is ", a + b);
```
Removing the semicolons does not alter the execution pattern or the return value.
```rust
let x = 10
let y = 3
write_line("The remainder is ", x % y)
```
The next example produces a compiler error because the `if` block returns a value of type `int` but
the `else` block does not return a value at all. Dust does not allow branches of the same `if/else`
statement to have different types. In this case, adding a semicolon after the `777` expression fixes
the error by supressing the value.
```rust
// !!! Compile Error !!!
let input = read_line()
let reward = if input == "42" {
write_line("You got it! Here's your reward.")
777 // <- We need a semicolon here
} else {
write_line(input, " is not the answer.")
}
```
#### Type System #### Type System
@ -296,14 +226,6 @@ Dust *does* have a `none` type, which should not be confused for being `null`-li
"unit" type in Rust, `none` exists as a type but not as a value. It indicates the lack of a value "unit" type in Rust, `none` exists as a type but not as a value. It indicates the lack of a value
from a function, expression or statement. A variable cannot be assigned to `none`. from a function, expression or statement. A variable cannot be assigned to `none`.
#### Immutability by Default
TODO
#### Memory Safety
TODO
### Basic Values ### Basic Values
Dust supports the following basic values: Dust supports the following basic values:
@ -324,10 +246,6 @@ singular values. Shorter strings are stored on the stack while longer strings ar
Dust offers built-in native functions that can manipulate strings by accessing their bytes or Dust offers built-in native functions that can manipulate strings by accessing their bytes or
reading them as a sequence of characters. reading them as a sequence of characters.
### Composite Values
TODO
## Previous Implementations ## Previous Implementations
Dust has gone through several iterations, each with its own design choices. It was originally Dust has gone through several iterations, each with its own design choices. It was originally

View File

@ -309,9 +309,6 @@ fn main() {
input, input,
}) = mode }) = mode
{ {
let run_span = span!(Level::TRACE, "CLI Run Mode");
let _run_guard = run_span.enter();
let (source, file_name) = get_source_and_file_name(input); let (source, file_name) = get_source_and_file_name(input);
let lexer = Lexer::new(&source); let lexer = Lexer::new(&source);
let mut compiler = match Compiler::new(lexer) { let mut compiler = match Compiler::new(lexer) {

View File

@ -25,7 +25,7 @@ mod type_checks;
pub use error::CompileError; pub use error::CompileError;
use parse_rule::{ParseRule, Precedence}; use parse_rule::{ParseRule, Precedence};
use tracing::{debug, info}; use tracing::{debug, info, span, Level};
use type_checks::{check_math_type, check_math_types}; use type_checks::{check_math_type, check_math_types};
use std::mem::replace; use std::mem::replace;
@ -135,12 +135,6 @@ impl<'src> Compiler<'src> {
pub fn new(mut lexer: Lexer<'src>) -> Result<Self, CompileError> { pub fn new(mut lexer: Lexer<'src>) -> Result<Self, CompileError> {
let (current_token, current_position) = lexer.next_token()?; let (current_token, current_position) = lexer.next_token()?;
info!(
"Begin chunk with {} at {}",
current_token.to_string(),
current_position.to_string()
);
Ok(Compiler { Ok(Compiler {
function_name: None, function_name: None,
r#type: FunctionType { r#type: FunctionType {
@ -172,8 +166,6 @@ impl<'src> Compiler<'src> {
/// will allow [`Compiler::function_name`] to be both the name used for recursive calls and the /// 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. /// 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 { pub fn finish(self, name: Option<impl Into<DustString>>) -> Chunk {
info!("End chunk");
let (instructions, positions): (SmallVec<[Instruction; 32]>, SmallVec<[Span; 32]>) = self let (instructions, positions): (SmallVec<[Instruction; 32]>, SmallVec<[Span; 32]>) = self
.instructions .instructions
.into_iter() .into_iter()
@ -205,6 +197,15 @@ impl<'src> Compiler<'src> {
/// [`CompileError`] if any are found. After calling this function, check its return value for /// [`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. /// an error, then call [`Compiler::finish`] to get the compiled chunk.
pub fn compile(&mut self) -> Result<(), CompileError> { pub fn compile(&mut self) -> Result<(), CompileError> {
let span = span!(Level::INFO, "Compiling");
let _enter = span.enter();
info!(
"Begin chunk with {} at {}",
self.current_token.to_string(),
self.current_position.to_string()
);
loop { loop {
self.parse(Precedence::None)?; self.parse(Precedence::None)?;
@ -219,6 +220,8 @@ impl<'src> Compiler<'src> {
} }
} }
info!("End chunk");
Ok(()) Ok(())
} }

View File

@ -16,6 +16,7 @@ pub use error::VmError;
pub use record::Record; pub use record::Record;
pub use run_action::RunAction; pub use run_action::RunAction;
pub use thread::{Thread, ThreadSignal}; pub use thread::{Thread, ThreadSignal};
use tracing::{span, Level};
use crate::{compile, Chunk, DustError, Value}; use crate::{compile, Chunk, DustError, Value};
@ -40,6 +41,9 @@ impl Vm {
} }
pub fn run(mut self) -> Option<Value> { pub fn run(mut self) -> Option<Value> {
let span = span!(Level::INFO, "Running");
let _enter = span.enter();
if self.threads.len() == 1 { if self.threads.len() == 1 {
return self.threads[0].run(); return self.threads[0].run();
} }

View File

@ -1,4 +1,5 @@
use dust_lang::*; use dust_lang::*;
use smallvec::smallvec;
#[test] #[test]
fn add_assign() { fn add_assign() {
@ -13,23 +14,18 @@ fn add_assign() {
value_parameters: None, value_parameters: None,
return_type: Type::Integer, return_type: Type::Integer,
}, },
vec![ smallvec![
(Instruction::load_constant(0, 0, false), Span(12, 13)), Instruction::load_constant(0, 0, false),
( Instruction::add(0, Argument::Register(0), Argument::Constant(2)),
Instruction::add(0, Argument::Register(0), Argument::Constant(2)), Instruction::get_local(1, 0),
Span(17, 19) Instruction::r#return(true)
),
(Instruction::get_local(1, 0), Span(23, 24)),
(Instruction::r#return(true), Span(24, 24))
], ],
vec![ smallvec![Span(12, 13), Span(17, 19), Span(23, 24), Span(24, 24)],
ConcreteValue::Integer(1), smallvec![Value::integer(1), Value::string("a"), Value::integer(2)],
ConcreteValue::string("a"), smallvec![Local::new(1, 0, true, Scope::default())],
ConcreteValue::Integer(2) vec![]
],
vec![Local::new(1, 0, true, Scope::default())]
)) ))
); );
assert_eq!(run(source), Ok(Some(ConcreteValue::Integer(3)))); assert_eq!(run(source), Ok(Some(Value::integer(3))));
} }