diff --git a/Cargo.lock b/Cargo.lock index b7fbc7d..0730842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,12 +79,29 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -106,12 +123,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "bitflags 1.3.2", + "textwrap", + "unicode-width", +] + [[package]] name = "clap" version = "4.5.20" @@ -174,17 +208,99 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "criterion" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +dependencies = [ + "atty", + "cast", + "clap 2.34.0", + "criterion-plot", + "csv", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "dust-cli" version = "0.5.0" dependencies = [ - "clap", + "clap 4.5.20", "colored", "dust-lang", "env_logger", @@ -198,18 +314,25 @@ name = "dust-lang" version = "0.5.0" dependencies = [ "annotate-snippets", - "bitflags", + "bitflags 2.6.0", "colored", - "env_logger", + "criterion", "getrandom", "log", "rand", "serde", "serde_json", + "slab", "smallvec", "smartstring", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -246,6 +369,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hash32" version = "0.2.1" @@ -275,6 +404,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "humantime" version = "2.1.0" @@ -287,6 +425,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -336,12 +483,55 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "postcard" version = "1.0.10" @@ -410,6 +600,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.11.1" @@ -454,6 +664,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -475,6 +694,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.214" @@ -498,6 +727,15 @@ dependencies = [ "serde", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -557,6 +795,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "unicode-ident" version = "1.0.13" @@ -581,6 +838,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -642,6 +909,47 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/README.md b/README.md index 71f90a6..f4f459f 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,86 @@ A programming language that is **fast**, **safe** and **easy to use**. -Dust's syntax, safety features and evaluation model are inspired by Rust. The instruction set, -optimization strategies and virtual machine are inspired by Lua and academic research in the field -(see the [Inspiration](README#Inspiration). Unlike Rust and most other compiled languages, Dust has -a very low time to execution. Unlike Lua and most other interpreted languages, Dust enforces static -typing during compilation, with a simple yet powerful type system that enhances clarity and prevents -bugs. +Dust is **statically typed** to ensure that each program is valid before it is run. It offers +compile times of less than 100 microseconds on modern hardware. As a **bytecode interpreter** with a +**register-based virtual machine**, Dust leverages compile-time safety guarantees and optimizations +along with beautiful syntax to deliver a unique set of features rarely found in other languages. It +is designed to deliver the best qualities of two disparate categories of programming language: the +highly optimized but slow-to-compile languages like Rust and C++ and the quick-to-start but often +slow and error-prone languages like Python and JavaScript. + +Dust's syntax, safety features and evaluation model are based on Rust. Its instruction set, +optimization strategies and virtual machine are based on Lua. Unlike Rust and other languages that +compile to machine code, Dust has a very low time to execution. Unlike Lua and most other +interpreted languages, Dust enforces static typing to improve clarity and prevent bugs. While some +languages currently offer high-level features and strict typing (e.g. TypeScript), Dust has a simple +approach to syntax that offers flexibility and expressiveness while still being *obvious* an +audience of programmers, even those who don't know the language. Dust is for programmers who prefer +their code to be simple and clear rather than complex and clever. + +## Goals + +This project has lofty goals. In addition to being a wishlist, these goals should be used to provide +a framework for driving the project forward and making decisions about what to prioritize. + +- **Fast Compilation**: Despite its compile-time abstractions, Dust should compile and start + executing quickly. The compilation time should feel negligible to the user. +- **Fast Execution**: Dust should be generally faster than Python, Ruby and NodeJS. It should be + competitive with other modern register-based VM languages like Lua and JavaScript Core. +- **Safety**: Static types should prevent runtime errors and improve code quality, offering a + superior development experience despite some additional constraints. +- **Approachability**: Dust should be easier to learn than Rust or C++. Its syntax should be + familiar to users of other C-like languages to the point that even a new user can read Dust code + and understand what it does. +- **Web Assembly Support**: The `dust` executable and, by extension, the `dust-lang` library, should + be able to able to compile to WebAssembly and Dust should be able to run in a browser with WASM + support. While running on the browser offers some fun opportunities, this is primarally a goal + because of WASM's potential to become a general-purpose cross-platform runtime. +- **Extended Type System**: Beyond specifying the types of variables and function arguments, Dust + should offer a rich yet simple type system that allows users to define their own types and compose + them with static guarantees about their identity and behavior. +- **Excellent Errors**: Dust should provide helpful error messages that guide the user to the source + of the problem and suggest a solution. Errors should be a helpful learning ressource for users + rather than a source of frustration. +- **High-Quality Documentation**: Dust's documentation should be easy to locate and understand. + Users should feel confident that the documentation is up-to-date and accurate. +- **All-In-One Binary**: The `dust` executable should aspire to be the only tool a user needs to run + Dust code, visualize Dust programs, compile them to intermediate representations, analyze runtime + behavior, run a REPL, format code and more as the scope of the project grows. Similar CLI tools + like Cargo and Bun have set a high standard for what a single executable can do. +- **Advanced Goals**: Dust could one day grow to the point that users will want to share their + libraries and distribute their programs. In the unlikely event that Dust becomes popular, it could + warrant an ecosystem consisting of package management with a central repository, a standard + library, a community of users and an organization to maintain the language. These are not within + the scope of the project at this time but it may be possible one day if the project is able to + realize its other goals. This is included here for maximum ambitiousness. + +## Non-Goals + +Some features are simply out of scope for Dust. As a project's design becomes an implementation, +decisions about what a project *will not* do are required to clarify the project's direction and +purpose for both the developers and the users. + +- **Machine Code Compilation**: Dust is not intended to compete with Rust or C++ in terms of runtime + performance. +- **Complex Abstractions**: Dust will not introduce users to new, exotic syntax or convoluted + patterns that reduce the clarity of a program. Dust will not support complex paradigm-specific + abstractions like inheritance or currying. Dust will remain neither object-oriented nor + functional, preferring to expand its features without committing to a single paradigm. +- **Gradual Typing**: Dust's compiler handles the complexities of *static* typing and all value and + variable types are known before a program runs. The VM is and should remain type-agnostic, leaving + it to the sole responsibility of execution. + +Dust uses a register-based VM with its own set of 32-bit instructions and a custom compiler to emit +the instructions. This should not be confused with a machine code compiler. Despite its compile-time +guarantees, Dust falls into the category of interpreted languages. Competing with the runtime +performance of Rust or C++ *is not* a goal. Competing with the approachability and simplicity of +those languages *is* a goal. On the other hand 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. ```rust write_line("Enter your name...") @@ -65,17 +139,24 @@ with a focus on stability and improvement. ### Syntax -Dust belongs to the C-like family of languages, with an imperative syntax that will be familiar to -many programmers. Dust code looks a lot like Ruby, JavaScript, TypeScript and other members of the -family but Rust is its primary point of reference for syntax. Rust was chosen as a syntax model +Dust belongs to the C-like family of languages[^5], with an imperative syntax that will be familiar +to many programmers. Dust code looks a lot like Ruby, JavaScript, TypeScript and other members of +the family but Rust is its primary point of reference for syntax. Rust was chosen as a syntax model because its imperative code is *obvious* and *familiar*. Those qualities are aligned with Dust's emphasis on safety and usability. However, some differences exist because Dust is a simpler language -that can tolerate more relaxed syntax. For example, Dust has more relaxed rules about semicolons: -they can be used to suppress values (like in Rust) but are not required at the end of every -statement. +that can tolerate more relaxed syntax. The most significant difference between Dust's syntax and +evaluation model and Rust's is the handling of semicolons. -In this example, these semicolons are optional. Because these `let` statements do not return a -value, the semicolons have nothing to suppress and are ignored. +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; @@ -84,7 +165,7 @@ let b = 2; write_line("The answer is ", a + b); ``` -One could write the above program without any semicolons at all. +Removing the semicolons does not alter the execution pattern. ```rust let x = 10 @@ -95,34 +176,71 @@ 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 return different types of values. In this case, adding a semicolon after the `777` -expression fixes the error by supressing the value. +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() - -if input == "42" { +let reward = if input == "42" { write_line("You got it! Here's your reward.") 777 } else { - write_line("That is not the answer.") -} + write_line(input, " is not the answer.") +}; ``` -Remember that even if some syntax is optional, that does not mean it should always be omitted or is -not useful. Aside from their practical use, semicolons provide a visual barrier between statements -written on the same line. Dust's design philosophy is to provide a balance between strictness and -expressiveness so that the language is applicable to a wide range of use cases. A web server with a -team of developers may prefer a more long-form style of code with lots of line breaks while a user -writing Dust on the command line may prefer a more terse style without sacrificing readability. +Understanding that semicolons suppress values is also important for understanding Dust's evaluation +model. Dust is composed of statements and expressions. If a statement ends in an expression without +a trailing semicolon, the statement evaluates to the value produced by that expression. However, if +the expression's value is suppressed with a semicolon, the statement does not evaluate to a value. +This is identical to Rust's evaluation model. That means that the following code will not compile: ```rust -let a = 0; let b = 1; let c = 2; let list = [a, b, c]; - -write_line("Here's our list: ", list) +// !!! Compile Error !!! +let a = { 40 + 2; } ``` +The `a` variable is assigned to the value produced by a block. The block contains an expression that +is suppressed by a semicolon, so the block does not evaluate to a value. Therefore, the `a` variable +would have to be uninitialized (which Dust does not allow) or result in a runtime error (which Dust +avoids at all costs). We can fix this code by movinf the semicolon to the end of the block. In this +position it suppresses the value of the entire `let` statement. The above examples showed that a +`let` statement never evaluates to a value, so the semicolon has no effect on the program's behavior +and could be omitted altogether. + +```rust +let a = { 40 + 2 }; // This is fine +``` + +Only the final expression in a block is returned. When a `let` statement is combined with an +`if/else` statement, the program can perform side effects before evaluating the value that will be +assigned to the variable. + +```rust +let random: int = random(0..100) +let is_even = if random == 99 { + write_line("We got a 99!") + + false +} else { + random % 2 == 0 +} + +is_even +``` + +If the above example were passed to Dust as a complete program, it would return a boolean value and +might print a message to the console (if the user is especially lucky). However, note that the +program could be modified to return no value by simply adding a semicolon at the end. + +Compared to JavaScript, Dust's evaluation model is more predictable, less error-prone and will never +trap the user into a frustating hunt for a missing semicolon. Compared to Rust, Dust's evaluation +model is essentialy the same but with more relaxed rules about semicolons. In JavaScript, semicolons +are both *required* and *meaningless*, which is a source of confusion for many developers. In Rust, +they are *required* and *meaningful*, which provides excellent consistency but lacks flexibility. + ### Safety #### Type System @@ -143,6 +261,8 @@ 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 from a function, expression or statement. A variable cannot be assigned to `none`. +#### Immutability by Default + #### Memory Safety @@ -172,7 +292,7 @@ reading them as a sequence of characters. ## Feature Progress This list is a rough outline of the features that are planned to be implemented as soon as possible. -*This is not an exhaustive list of all planned features.* This list is updated and rearranged to +*This is **not** an exhaustive list of all planned features.* This list is updated and rearranged to maintain a docket of what is being worked on, what is coming next and what can be revisited later. - [X] Lexer @@ -180,7 +300,9 @@ maintain a docket of what is being worked on, what is coming next and what can b - [X] VM - [X] Disassembler (for chunk debugging) - [ ] Formatter -- [ ] REPL +- [ ] CLI REPL +- [X] Compile dust's binary and library to WASM +- [ ] Browser-based REPL - CLI - [X] Run source - [X] Compile source to a chunk and show disassembly @@ -248,13 +370,19 @@ maintain a docket of what is being worked on, what is coming next and what can b - [X] `read_line` - [X] `write` - [X] `write_line` - - String Functions - - List Functions - - Map Functions - - Math Functions - - Filesystem Functions - - Network Functions - - System Functions + - Miniature Standard Library of Native Functions + - [ ] Byte Functions + - [ ] Character Functions + - [ ] Float Functions + - [ ] Integer Functions + - [ ] String Functions + - [ ] List Functions + - [ ] Map Functions + - [ ] Math Functions + - [ ] Filesystem Functions + - [ ] Network Functions + - [ ] System Functions + - [ ] Randomization Functions ## Implementation @@ -305,7 +433,7 @@ arguments are stored in its locals list. Before the function is run, the VM must to values by filling locals' corresponding registers. Instead of copying the arguments, the VM uses a pointer to one of the parent's registers or constants. -#### Optimizing +#### Instruction Optimization When generating instructions for a register-based virtual machine, there are opportunities to optimize the generated code by using fewer instructions or fewer registers. While it is best to @@ -481,3 +609,4 @@ Dust is licensed under the GNU General Public License v3.0. See the `LICENSE` fi [^3]: [A No-Frills Introduction to Lua 5.1 VM Instructions](https://www.mcours.net/cours/pdf/hasclic3/hasssclic818.pdf) [^4]: [A Performance Survey on Stack-based and Register-based Virtual Machines](https://arxiv.org/abs/1611.00467) [^5]: [List of C-family programming languages](https://en.wikipedia.org/wiki/List_of_C-family_programming_languages) +[^6]: [ripgrep is faster than {grep, ag, git grep, ucg, pt, sift}](https://blog.burntsushi.net/ripgrep/#mechanics) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..0684cfd --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +RUSTFLAGS="\ + -C collapse-macro-debuginfo=false \ + -C default-linker-libraries=false \ + -C embed-bitcode=true \ + -C force-frame-pointers=true \ + -C force-unwind-tables=false \ + -C passes=mem2reg \ + -C linker-plugin-lto" + cargo build --release --package dust-cli diff --git a/dust-cli/src/main.rs b/dust-cli/src/main.rs index cd6c812..35c9796 100644 --- a/dust-cli/src/main.rs +++ b/dust-cli/src/main.rs @@ -196,9 +196,9 @@ fn main() { } } + let chunk = compiler.finish(None, None); let compile_end = start_time.elapsed(); - let chunk = compiler.finish(None, None); let mut vm = Vm::new(&source, &chunk, None); match vm.run() { diff --git a/dust-lang/Cargo.toml b/dust-lang/Cargo.toml index c38940f..ed11c85 100644 --- a/dust-lang/Cargo.toml +++ b/dust-lang/Cargo.toml @@ -23,6 +23,87 @@ smartstring = { version = "1.0.1", features = [ "serde", ], default-features = false } bitflags = { version = "2.6.0", features = ["serde"] } +slab = "0.4.9" [dev-dependencies] -env_logger = "0.11.5" +criterion = { version = "0.3.4", features = ["html_reports"] } + +[[bench]] +name = "addictive_addition" +harness = false + +[[test]] +name = "logic_and" +path = "tests/logic/and.rs" + +[[test]] +name = "logic_and_and" +path = "tests/logic/and_and.rs" + +[[test]] +name = "logic_or" +path = "tests/logic/or.rs" + +[[test]] +name = "logic_variables" +path = "tests/logic/variables.rs" + +[[test]] +name = "math_add" +path = "tests/math/add.rs" + +[[test]] +name = "math_add_assign" +path = "tests/math/add_assign.rs" + +[[test]] +name = "math_add_errors" +path = "tests/math/add_errors.rs" + +[[test]] +name = "math_divide" +path = "tests/math/divide.rs" + +[[test]] +name = "math_divide_assign" +path = "tests/math/divide_assign.rs" + +[[test]] +name = "math_divide_erros" +path = "tests/math/divide_errors.rs" + +[[test]] +name = "math_modulo" +path = "tests/math/modulo.rs" + +[[test]] +name = "math_modulo_assign" +path = "tests/math/modulo_assign.rs" + +[[test]] +name = "math_modulo_errors" +path = "tests/math/modulo_errors.rs" + +[[test]] +name = "math_multiply" +path = "tests/math/multiply.rs" + +[[test]] +name = "math_multiply_assign" +path = "tests/math/multiply_assign.rs" + +[[test]] +name = "math_multiply_errors" +path = "tests/math/multiply_errors.rs" + +[[test]] +name = "math_subtract" +path = "tests/math/subtract.rs" + +[[test]] +name = "math_subtract_assign" +path = "tests/math/subtract_assign.rs" + +[[test]] +name = "math_subtract_errors" +path = "tests/math/subtract_errors.rs" diff --git a/dust-lang/benches/addictive_addition.rs b/dust-lang/benches/addictive_addition.rs new file mode 100644 index 0000000..964939f --- /dev/null +++ b/dust-lang/benches/addictive_addition.rs @@ -0,0 +1,29 @@ +use std::time::Duration; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dust_lang::run; + +const SOURCE: &str = " + let mut i = 0 + + while i < 5_000_000 { + i += 1 + } +"; + +fn addictive_addition(source: &str) { + let _ = run(source).unwrap(); +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("addictive_addition"); + + group.measurement_time(Duration::from_secs(15)); + group.bench_function("addictive_addition", |b| { + b.iter(|| addictive_addition(black_box(SOURCE))) + }); + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/dust-lang/src/instruction/mod.rs b/dust-lang/src/instruction/mod.rs index ef550da..b4ac282 100644 --- a/dust-lang/src/instruction/mod.rs +++ b/dust-lang/src/instruction/mod.rs @@ -655,7 +655,7 @@ impl Instruction { if is_positive { format!("JUMP +{offset}") } else { - format!("JUMP -{offset}") + format!("JUMP -{}", offset - 1) } } Operation::Call => { diff --git a/dust-lang/src/vm.rs b/dust-lang/src/vm.rs index 25106eb..ca8a945 100644 --- a/dust-lang/src/vm.rs +++ b/dust-lang/src/vm.rs @@ -4,7 +4,8 @@ use std::{ io, }; -use smallvec::{smallvec, SmallVec}; +use slab::Slab; +use smallvec::SmallVec; use crate::{ compile, instruction::*, AbstractValue, AnnotatedError, Argument, Chunk, ConcreteValue, @@ -23,7 +24,7 @@ pub fn run(source: &str) -> Result, DustError> { /// See the [module-level documentation](index.html) for more information. #[derive(Debug)] pub struct Vm<'a> { - stack: SmallVec<[Register; 64]>, + stack: Slab, chunk: &'a Chunk, parent: Option<&'a Vm<'a>>, @@ -35,9 +36,15 @@ pub struct Vm<'a> { impl<'a> Vm<'a> { pub fn new(source: &'a str, chunk: &'a Chunk, parent: Option<&'a Vm<'a>>) -> Self { + let mut stack = Slab::with_capacity(chunk.stack_size()); + + for _ in 0..chunk.stack_size() { + stack.insert(Register::Empty); + } + Self { chunk, - stack: smallvec![Register::Empty; chunk.stack_size()], + stack, parent, ip: 0, last_assigned_register: None, @@ -511,10 +518,10 @@ impl<'a> Vm<'a> { } = CallNative::from(&instruction); let first_argument_index = (destination - argument_count) as usize; let argument_range = first_argument_index..destination as usize; - let argument_registers = &self.stack[argument_range]; let mut arguments: SmallVec<[ValueRef; 4]> = SmallVec::new(); - for register in argument_registers { + for register_index in argument_range { + let register = &self.stack[register_index]; let value = match register { Register::Value(value) => value.to_ref(), Register::Pointer(pointer) => { @@ -639,23 +646,20 @@ impl<'a> Vm<'a> { fn open_register(&self, register_index: u8) -> Result { let register_index = register_index as usize; - assert!( - register_index < self.stack.len(), - "VM Error: Register index out of bounds" - ); + if register_index < self.stack.len() { + let register = &self.stack[register_index]; - let register = &self.stack[register_index]; - - log::trace!("Open R{register_index} to {register}"); - - match register { - Register::Value(value) => Ok(value.to_ref()), - Register::Pointer(pointer) => self.follow_pointer(*pointer), - Register::Empty => Err(VmError::EmptyRegister { - index: register_index, - position: self.current_position(), - }), + return match register { + Register::Value(value) => Ok(value.to_ref()), + Register::Pointer(pointer) => self.follow_pointer(*pointer), + Register::Empty => Err(VmError::EmptyRegister { + index: register_index, + position: self.current_position(), + }), + }; } + + panic!("VM Error: Register index out of bounds"); } fn open_register_allow_empty(&self, register_index: u8) -> Result, VmError> { @@ -688,12 +692,11 @@ impl<'a> Vm<'a> { } ); - let new_ip = if is_positive { - self.ip + offset + if is_positive { + self.ip += offset } else { - self.ip - offset - 1 - }; - self.ip = new_ip; + self.ip -= offset + 1 + } } /// DRY helper to get a value from an Argument @@ -708,71 +711,55 @@ impl<'a> Vm<'a> { Ok(value_ref) } - #[inline(always)] fn set_register(&mut self, to_register: u8, register: Register) -> Result<(), VmError> { self.last_assigned_register = Some(to_register); - let to_register = to_register as usize; - assert!( - self.stack.len() > to_register, - "VM Error: Register index out of bounds" - ); + if to_register < self.stack.len() { + self.stack[to_register] = register; - self.stack[to_register] = register; + return Ok(()); + } - Ok(()) + panic!("VM Error: Register index out of bounds"); } - #[inline(always)] fn get_constant(&self, constant_index: u8) -> &ConcreteValue { let constant_index = constant_index as usize; - let constants = self.chunk.constants(); + let constants = self.chunk.constants().as_slice(); - assert!( - constant_index < constants.len(), - "VM Error: Constant index out of bounds" - ); + if constant_index < constants.len() { + return &constants[constant_index]; + } - &constants[constant_index] + panic!("VM Error: Constant index out of bounds"); } - #[inline(always)] fn get_local_register(&self, local_index: u8) -> Result { let local_index = local_index as usize; - let locals = self.chunk.locals(); + let locals = self.chunk.locals().as_slice(); - assert!( - local_index < locals.len(), - "VM Error: Local index out of bounds" - ); + if local_index < locals.len() { + let register_index = locals[local_index].register_index; - let register_index = locals[local_index].register_index; + return Ok(register_index); + } - Ok(register_index) + panic!("VM Error: Local index out of bounds"); } - #[inline(always)] fn read(&mut self) -> Instruction { - let instructions = self.chunk.instructions(); + let instructions = self.chunk.instructions().as_slice(); - assert!( - self.ip < instructions.len(), - "{}", - DustError::runtime( - VmError::InstructionIndexOutOfBounds { - index: self.ip, - position: self.current_position() - }, - self.source, - ) - ); + if self.ip < instructions.len() { + let (instruction, _) = instructions[self.ip]; - let (instruction, _) = instructions[self.ip]; + self.ip += 1; - self.ip += 1; + return instruction; + } - instruction + panic!("VM Error: Instruction pointer out of bounds"); } } diff --git a/dust-lang/tests/logic_and.rs b/dust-lang/tests/logic/and.rs similarity index 100% rename from dust-lang/tests/logic_and.rs rename to dust-lang/tests/logic/and.rs diff --git a/dust-lang/tests/logic_and_and.rs b/dust-lang/tests/logic/and_and.rs similarity index 100% rename from dust-lang/tests/logic_and_and.rs rename to dust-lang/tests/logic/and_and.rs diff --git a/dust-lang/tests/logic_or.rs b/dust-lang/tests/logic/or.rs similarity index 100% rename from dust-lang/tests/logic_or.rs rename to dust-lang/tests/logic/or.rs diff --git a/dust-lang/tests/logic_variables.rs b/dust-lang/tests/logic/variables.rs similarity index 100% rename from dust-lang/tests/logic_variables.rs rename to dust-lang/tests/logic/variables.rs diff --git a/dust-lang/tests/math_add.rs b/dust-lang/tests/math/add.rs similarity index 100% rename from dust-lang/tests/math_add.rs rename to dust-lang/tests/math/add.rs diff --git a/dust-lang/tests/math_add_assign.rs b/dust-lang/tests/math/add_assign.rs similarity index 100% rename from dust-lang/tests/math_add_assign.rs rename to dust-lang/tests/math/add_assign.rs diff --git a/dust-lang/tests/math_add_errors.rs b/dust-lang/tests/math/add_errors.rs similarity index 100% rename from dust-lang/tests/math_add_errors.rs rename to dust-lang/tests/math/add_errors.rs diff --git a/dust-lang/tests/math_divide.rs b/dust-lang/tests/math/divide.rs similarity index 100% rename from dust-lang/tests/math_divide.rs rename to dust-lang/tests/math/divide.rs diff --git a/dust-lang/tests/math_divide_assign.rs b/dust-lang/tests/math/divide_assign.rs similarity index 100% rename from dust-lang/tests/math_divide_assign.rs rename to dust-lang/tests/math/divide_assign.rs diff --git a/dust-lang/tests/math_divide_errors.rs b/dust-lang/tests/math/divide_errors.rs similarity index 100% rename from dust-lang/tests/math_divide_errors.rs rename to dust-lang/tests/math/divide_errors.rs diff --git a/dust-lang/tests/math_modulo.rs b/dust-lang/tests/math/modulo.rs similarity index 100% rename from dust-lang/tests/math_modulo.rs rename to dust-lang/tests/math/modulo.rs diff --git a/dust-lang/tests/math_modulo_assign.rs b/dust-lang/tests/math/modulo_assign.rs similarity index 100% rename from dust-lang/tests/math_modulo_assign.rs rename to dust-lang/tests/math/modulo_assign.rs diff --git a/dust-lang/tests/math_modulo_errors.rs b/dust-lang/tests/math/modulo_errors.rs similarity index 100% rename from dust-lang/tests/math_modulo_errors.rs rename to dust-lang/tests/math/modulo_errors.rs diff --git a/dust-lang/tests/math_multiply.rs b/dust-lang/tests/math/multiply.rs similarity index 100% rename from dust-lang/tests/math_multiply.rs rename to dust-lang/tests/math/multiply.rs diff --git a/dust-lang/tests/math_multiply_assign.rs b/dust-lang/tests/math/multiply_assign.rs similarity index 100% rename from dust-lang/tests/math_multiply_assign.rs rename to dust-lang/tests/math/multiply_assign.rs diff --git a/dust-lang/tests/math_multiply_errors.rs b/dust-lang/tests/math/multiply_errors.rs similarity index 100% rename from dust-lang/tests/math_multiply_errors.rs rename to dust-lang/tests/math/multiply_errors.rs diff --git a/dust-lang/tests/math_subtract.rs b/dust-lang/tests/math/subtract.rs similarity index 100% rename from dust-lang/tests/math_subtract.rs rename to dust-lang/tests/math/subtract.rs diff --git a/dust-lang/tests/math_subtract_assign.rs b/dust-lang/tests/math/subtract_assign.rs similarity index 100% rename from dust-lang/tests/math_subtract_assign.rs rename to dust-lang/tests/math/subtract_assign.rs diff --git a/dust-lang/tests/math_subtract_errors.rs b/dust-lang/tests/math/subtract_errors.rs similarity index 100% rename from dust-lang/tests/math_subtract_errors.rs rename to dust-lang/tests/math/subtract_errors.rs diff --git a/examples/guessing_game.ds b/examples/guessing_game.ds index ae31ed1..d3a4378 100644 --- a/examples/guessing_game.ds +++ b/examples/guessing_game.ds @@ -1,12 +1,12 @@ write_line("Guess the number.") -let secret_number = random(0..100); +let secret_number = random(0..100) loop { write_line("Input your guess.") - let input = io.read_line(); - let guess = int.parse(input); + let input = io.read_line() + let guess = int.parse(input) if guess < secret_number { io.write_line("Too low!")