Break up tests; Write docs
This commit is contained in:
parent
cdd76618cb
commit
1c32cd0956
314
Cargo.lock
generated
314
Cargo.lock
generated
@ -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"
|
||||
|
209
README.md
209
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
|
||||
|
||||
<!-- TODO: Introduce Dust's approach to memory management and garbage collection. -->
|
||||
@ -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)
|
||||
|
9
build.sh
Normal file
9
build.sh
Normal file
@ -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
|
@ -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() {
|
||||
|
@ -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"
|
||||
|
29
dust-lang/benches/addictive_addition.rs
Normal file
29
dust-lang/benches/addictive_addition.rs
Normal file
@ -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);
|
@ -655,7 +655,7 @@ impl Instruction {
|
||||
if is_positive {
|
||||
format!("JUMP +{offset}")
|
||||
} else {
|
||||
format!("JUMP -{offset}")
|
||||
format!("JUMP -{}", offset - 1)
|
||||
}
|
||||
}
|
||||
Operation::Call => {
|
||||
|
@ -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<Option<ConcreteValue>, DustError> {
|
||||
/// See the [module-level documentation](index.html) for more information.
|
||||
#[derive(Debug)]
|
||||
pub struct Vm<'a> {
|
||||
stack: SmallVec<[Register; 64]>,
|
||||
stack: Slab<Register>,
|
||||
|
||||
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<ValueRef, VmError> {
|
||||
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];
|
||||
|
||||
log::trace!("Open R{register_index} to {register}");
|
||||
|
||||
match register {
|
||||
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<Option<ValueRef>, 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;
|
||||
|
||||
Ok(())
|
||||
return 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"
|
||||
);
|
||||
|
||||
&constants[constant_index]
|
||||
if constant_index < constants.len() {
|
||||
return &constants[constant_index];
|
||||
}
|
||||
|
||||
panic!("VM Error: Constant index out of bounds");
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_local_register(&self, local_index: u8) -> Result<u8, VmError> {
|
||||
let local_index = local_index as usize;
|
||||
let locals = self.chunk.locals();
|
||||
|
||||
assert!(
|
||||
local_index < locals.len(),
|
||||
"VM Error: Local index out of bounds"
|
||||
);
|
||||
let locals = self.chunk.locals().as_slice();
|
||||
|
||||
if local_index < locals.len() {
|
||||
let register_index = locals[local_index].register_index;
|
||||
|
||||
Ok(register_index)
|
||||
return Ok(register_index);
|
||||
}
|
||||
|
||||
panic!("VM Error: Local index out of bounds");
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn read(&mut self) -> Instruction {
|
||||
let instructions = self.chunk.instructions();
|
||||
|
||||
assert!(
|
||||
self.ip < instructions.len(),
|
||||
"{}",
|
||||
DustError::runtime(
|
||||
VmError::InstructionIndexOutOfBounds {
|
||||
index: self.ip,
|
||||
position: self.current_position()
|
||||
},
|
||||
self.source,
|
||||
)
|
||||
);
|
||||
let instructions = self.chunk.instructions().as_slice();
|
||||
|
||||
if self.ip < instructions.len() {
|
||||
let (instruction, _) = instructions[self.ip];
|
||||
|
||||
self.ip += 1;
|
||||
|
||||
instruction
|
||||
return instruction;
|
||||
}
|
||||
|
||||
panic!("VM Error: Instruction pointer out of bounds");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!")
|
||||
|
Loading…
Reference in New Issue
Block a user