Compare commits

...

8 Commits

Author SHA1 Message Date
mat ess bcee1cf160 Add builtins 2023-07-24 12:23:13 -04:00
mat ess e84b351daa Reorganize 2023-07-24 12:22:57 -04:00
mat ess 501dfff220 Implement running main 2023-07-21 09:40:38 -04:00
mat ess a1e5aef811 Add evaluation 2023-07-21 09:24:04 -04:00
mat ess 8af9855a27 Rework error handling and main 2023-07-19 23:03:15 -04:00
mat ess 2ccc3e8ef1 Reorganize 2023-07-02 21:37:12 -04:00
mat ess 056f6d58bf Update manifests 2023-07-02 21:37:06 -04:00
mat ess d3a8c822fb Update docs and example 2023-07-02 21:36:57 -04:00
18 changed files with 717 additions and 347 deletions

48
Cargo.lock generated
View File

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.71"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
[[package]]
name = "ascii-canvas"
@ -207,9 +207,9 @@ dependencies = [
[[package]]
name = "lalrpop"
version = "0.19.9"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34313ec00c2eb5c3c87ca6732ea02dcf3af99c3ff7a8fb622ffb99c9d860a87"
checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8"
dependencies = [
"ascii-canvas",
"bit-set",
@ -221,7 +221,7 @@ dependencies = [
"petgraph",
"pico-args",
"regex",
"regex-syntax 0.6.29",
"regex-syntax",
"string_cache",
"term",
"tiny-keccak",
@ -230,9 +230,9 @@ dependencies = [
[[package]]
name = "lalrpop-util"
version = "0.19.9"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c1f7869c94d214466c5fd432dfed12c379fd87786768d36455892d46b18edd"
checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d"
dependencies = [
"regex",
]
@ -281,7 +281,9 @@ dependencies = [
"anyhow",
"lalrpop",
"lalrpop-util",
"pico-args",
"regex",
"thiserror",
]
[[package]]
@ -340,9 +342,9 @@ dependencies = [
[[package]]
name = "pico-args"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "precomputed-hash"
@ -352,18 +354,18 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro2"
version = "1.0.56"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [
"proc-macro2",
]
@ -396,15 +398,9 @@ checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.1",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.1"
@ -464,9 +460,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.15"
version = "2.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
dependencies = [
"proc-macro2",
"quote",
@ -486,18 +482,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.40"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",

View File

@ -4,9 +4,11 @@ version = "0.1.0"
edition = "2021"
[build-dependencies]
lalrpop = "0.19.9"
lalrpop = "0.20.0"
[dependencies]
anyhow = "1.0.71"
lalrpop-util = { version = "0.19.9", features = ["lexer"] }
anyhow = "1.0.72"
lalrpop-util = { version = "0.20.0", features = ["lexer"] }
pico-args = { version = "0.5.0", features = ["eq-separator", "short-space-opt", "combined-flags"] }
regex = "1"
thiserror = "1.0.43"

View File

@ -1,37 +1,9 @@
# mul
mat's untitled language
## mat's untitled language
## goals
see [`example.mul`](/example.mul) for a syntax sample.
### start simple and grow
see [`GOALS.md`](/docs/GOALS.md) for guiding principles and inspirations.
while mul's implementation is still getting started, i want to implement features atomically, one at a time, in an end to end manner.
for example, we'll start with a simple system-f/bidirectional system, implement a parser, typechecker, interpreter, and code generator, then gradually layer features onto that, supporting them at every "level" of the compiler.
### orthogonal and discoverable
avoid overly redundant or unrelated functionality. make the core set of functionality well documented and easy to inspect. taking cues here from [fish shell](https://fishshell.com/docs/current/design.html).
### robust static type system
without going to the extent of dependent types, mul's type system should be expressive and flexible.
### application-friendly ergonomics without sacrificing performance
mul will take cues from visions for a "smaller" rust that is able to focus on a different set of problems by not specializing in systems programming.
[Notes on a smaller Rust](https://without.boats/blog/notes-on-a-smaller-rust/)
[Revisiting a 'smaller Rust'](https://without.boats/blog/revisiting-a-smaller-rust/)
[Rust's Ugly Syntax](https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html)
in short, we want to take parts of rust's syntax and semantics that work well and are amenable to both machine and human analysis, while leaving behind the components that are designed around control of memory layout + representation, explicit mentions of ownership and borrowing, and other more complicated concepts.
while not prioritizing it in the early implementation, mul will try to learn from rust (and its progeny) to sacrifice as little performance as possible while still offering a substantially simpler interface.
### incremental, interactive, intelligent
parsing, typechecking, evaluating, compiling, etc should be usable across a batch compiler, interactive REPL, and IDE tooling. program information should be preserved where possible.
see [`ROADMAP.md`](/docs/ROADMAP.md) for specific implementation plans and progress.

61
docs/GOALS.md Normal file
View File

@ -0,0 +1,61 @@
# goals
## learning!
the primary goal with mul is for me to work through implementing a compiler, something i have long wanted to do, but never actually succeeded in.
## start simple and grow
while mul's implementation is still getting started, i want to implement features atomically, one at a time, in an end to end manner.
for example, we'll start with a simple system-f/bidirectional system, implement a parser, typechecker, interpreter, and code generator, then gradually layer features onto that, supporting them at every "level" of the compiler.
## orthogonal and discoverable
avoid overly redundant or unrelated functionality. make the core set of functionality well documented and easy to inspect.
inspiration:
- [fish shell design](https://fishshell.com/docs/current/design.html)
## robust static type system
without going to the extent of dependent types, mul's type system should be expressive and flexible.
### experiment with capabilities and/or effects
one specific feature of the type system will be a focus on safety and security through control of side effects. two interesting avenues to explore for these aspects will be capabilities and effect handling.
capabilities:
- [Lambda Capabilities](https://roscidus.com/blog/blog/2023/04/26/lambda-capabilities)
- [What Are Capabilities?](http://habitatchronicles.com/2017/05/what-are-capabilities/)
- [Spritely - What is CapTP, and what does it enable?](https://spritelyproject.org/news/what-is-captp.html)
- [Cap'n Proto](https://capnproto.org/)
effects:
- [Unison's Introduction to Abilities: A Mental Model](https://www.unison-lang.org/learn/fundamentals/abilities/)
- [Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/)
- [The Koka Programming Language](https://koka-lang.github.io/koka/doc/book.html)
- [Eff's An Introduction to Algebraic Effects and Handlers](https://www.eff-lang.org/handlers-tutorial.pdf)
- [Exotic Programming Ideas: Effect Systems](https://www.stephendiehl.com/posts/exotic03.html)
open question: how should this design interact with a trait system? can/should effects subsume traits?
## application-friendly ergonomics without sacrificing performance
mul will take cues from visions for a "smaller" rust that is able to focus on a different set of problems by not specializing in systems programming.
inspiration:
- [Notes on a smaller Rust](https://without.boats/blog/notes-on-a-smaller-rust/)
- [Revisiting a 'smaller Rust'](https://without.boats/blog/revisiting-a-smaller-rust/)
- [Rust's Ugly Syntax](https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html)
in short, we want to take parts of rust's syntax and semantics that work well and are amenable to both machine and human analysis, while leaving behind the components that are designed around control of memory layout + representation, explicit mentions of ownership and borrowing, and other more complicated concepts.
while not prioritizing it in the early implementation, mul will try to learn from rust (and its progeny) to sacrifice as little performance as possible while still offering a substantially simpler interface.
## incremental, interactive, intelligent
parsing, typechecking, evaluating, compiling, etc should be usable across a batch compiler, interactive REPL, and IDE tooling. program information should be preserved where possible.
inspiration:
- [Query-based compiler architectures](https://ollef.github.io/blog/posts/query-based-compilers.html)

View File

@ -14,6 +14,9 @@
- [x] integers
- [ ] floating point numbers
- [ ] text
- [ ] items
- [x] function definitions
- [ ] type definitions
- [ ] functions
- [x] lambdas / closures
- [ ] generic functions
@ -24,5 +27,6 @@
- [ ] sum types
- [ ] variant types
- [ ] pattern matching
- [ ] traits
- [ ] traits (?)
- [ ] operator overloading
- [ ] capabilities/effects

View File

@ -1,9 +1,8 @@
let main = || {
fn main() {
let sum_equals = |x, y, expected| {
let sum = _add(x, y);
_equals(sum, expected)
};
let result = sum_equals(1, 2, 3);
print(result)
};
let sum_equals = |x, y, expected| {
let sum = add(x, y);
equals(sum, expected)
};
_print(result)
}

View File

@ -10,11 +10,11 @@
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1681680516,
"narHash": "sha256-EB8Adaeg4zgcYDJn9sR6UMjN/OHdIiMMK19+3LmmXQY=",
"lastModified": 1684468982,
"narHash": "sha256-EoC1N5sFdmjuAP3UOkyQujSOT6EdcXTnRw8hPjJkEgc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "54b63c8eae4c50172cb50b612946ff1d2bc1c75c",
"rev": "99de890b6ef4b4aab031582125b6056b792a4a30",
"type": "github"
},
"original": {
@ -60,11 +60,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1680392223,
"narHash": "sha256-n3g7QFr85lDODKt250rkZj2IFS3i4/8HBU2yKHO3tqw=",
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "dcc36e45d054d7bb554c9cdab69093debd91a0b5",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"type": "github"
},
"original": {
@ -74,12 +74,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1678901627,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@ -126,11 +129,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1681920287,
"narHash": "sha256-+/d6XQQfhhXVfqfLROJoqj3TuG38CAeoT6jO1g9r1k0=",
"lastModified": 1684570954,
"narHash": "sha256-FX5y4Sm87RWwfu9PI71XFvuRpZLowh00FQpIJ1WfXqE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "645bc49f34fa8eff95479f0345ff57e55b53437e",
"rev": "3005f20ce0aaa58169cdee57c8aa12e5f1b6e1b3",
"type": "github"
},
"original": {
@ -143,11 +146,11 @@
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1680213900,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
},
"original": {
@ -185,11 +188,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1681831107,
"narHash": "sha256-pXl3DPhhul9NztSetUJw2fcN+RI3sGOYgKu29xpgnqw=",
"lastModified": 1684195081,
"narHash": "sha256-IKnQUSBhQTChFERxW2AzuauVpY1HRgeVzAjNMAA4B6I=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "b7ca8f6fff42f6af75c17f9438fed1686b7d855d",
"rev": "96eabec58248ed8f4b0ad59e7ce9398018684fdc",
"type": "github"
},
"original": {
@ -218,11 +221,11 @@
]
},
"locked": {
"lastModified": 1680488274,
"narHash": "sha256-0vYMrZDdokVmPQQXtFpnqA2wEgCCUXf5a3dDuDVshn0=",
"lastModified": 1683080331,
"narHash": "sha256-nGDvJ1DAxZIwdn6ww8IFwzoHb2rqBP4wv/65Wt5vflk=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "7ec2ff598a172c6e8584457167575b3a1a5d80d8",
"rev": "d59c3fa0cba8336e115b376c2d9e91053aa59e56",
"type": "github"
},
"original": {
@ -230,6 +233,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@ -59,6 +59,11 @@
clippy
# profiling
cargo-flamegraph
# other tooling
cargo-audit
cargo-deny
cargo-edit
cargo-license
;
rustfmt = pkgs.rustfmt.overrideAttrs (old: {
preFixup = pkgs.lib.optionalString pkgs.stdenv.isDarwin ''

132
src/builtins.rs Normal file
View File

@ -0,0 +1,132 @@
use std::fmt::Write;
use thiserror::Error;
use crate::evaluate::{Dictionary, Value};
#[derive(Debug, Copy, Clone)]
pub enum Builtin {
Print,
Add,
Sub,
Mul,
Div,
Equals,
GreaterThan,
LessThan,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Tried to {0} a non-integer: {1}")]
NonInteger(&'static str, Value),
}
type Result<T> = std::result::Result<T, Error>;
fn unwrap_int_value(op_name: &'static str) -> impl Fn(Value) -> Result<i64> {
move |value| match value {
Value::Integer(i) => Ok(i),
_ => Err(Error::NonInteger(op_name, value)),
}
}
macro_rules! int_op {
($arguments:expr, $op:tt, $op_name:expr) => {
Value::Integer(
$arguments
.into_iter()
.map(unwrap_int_value($op_name))
.reduce(|acc, v| Ok(acc? $op v?))
.unwrap()?,
)
};
}
macro_rules! int_cmp {
($arguments:expr, $op:tt) => {{
let mut arguments = $arguments;
let unwrap = unwrap_int_value("compare");
let y = unwrap(arguments.pop().unwrap())?;
let x = unwrap(arguments.pop().unwrap())?;
Value::Boolean(x $op y)
}};
}
impl Builtin {
pub fn dictionary() -> Dictionary<Value> {
[
("_print", Builtin::Print),
("_add", Builtin::Add),
("_sub", Builtin::Sub),
("_mul", Builtin::Mul),
("_div", Builtin::Div),
("_equals", Builtin::Equals),
("_greaterthan", Builtin::GreaterThan),
("_lessthan", Builtin::LessThan),
]
.map(|(name, builtin)| (name.to_string(), Value::Builtin(builtin)))
.into()
}
pub fn min_parameters(&self) -> usize {
match self {
Builtin::Print => 0,
Builtin::Add
| Builtin::Sub
| Builtin::Mul
| Builtin::Div
| Builtin::Equals
| Builtin::GreaterThan
| Builtin::LessThan => 2,
}
}
pub fn max_parameters(&self) -> Option<usize> {
match self {
Builtin::Print
| Builtin::Add
| Builtin::Sub
| Builtin::Mul
| Builtin::Div
| Builtin::Equals => None,
Builtin::GreaterThan | Builtin::LessThan => Some(2),
}
}
pub fn call(&self, arguments: Vec<Value>) -> Result<Value> {
let result = match self {
Builtin::Print => {
let mut output = String::new();
let mut arguments = arguments.into_iter().peekable();
while let Some(term) = arguments.next() {
if arguments.peek().is_some() {
write!(output, "{term} ").expect("Error during printing");
} else {
write!(output, "{term}").expect("Error during printing");
}
}
println!("{output}");
Value::Unit
}
Builtin::Add => int_op!(arguments, +, "add"),
Builtin::Sub => int_op!(arguments, -, "subtract"),
Builtin::Mul => int_op!(arguments, *, "multiply"),
Builtin::Div => int_op!(arguments, /, "divide"),
Builtin::Equals => {
let mut arguments = arguments;
let y = arguments.pop().unwrap();
let x = arguments.pop().unwrap();
let b = match (x, y) {
(Value::Integer(x), Value::Integer(y)) => x == y,
(Value::Boolean(x), Value::Boolean(y)) => x == y,
(Value::Unit, Value::Unit) => true,
_ => false,
};
Value::Boolean(b)
}
Builtin::GreaterThan => int_cmp!(arguments, >),
Builtin::LessThan => int_cmp!(arguments, <),
};
Ok(result)
}
}

60
src/cli.rs Normal file
View File

@ -0,0 +1,60 @@
use std::{ffi::OsString, path::PathBuf};
use pico_args::Arguments;
use thiserror::Error;
pub const USAGE: &str = "
USAGE:
mul [-h|--help] (parse <FILE> | evaluate <FILE> | repl)
FLAGS:
-h, --help Prints help information (this text)
COMMANDS:
parse <FILE> Parse the given file and print a serialized AST to stdout
evaluate, eval <FILE> Evaluate the given file
repl Start the Read-Evaluate-Print-Loop
";
#[derive(Debug, Error)]
pub enum Error {
#[error("Problem while parsing arguments, {0}")]
ArgsError(#[from] pico_args::Error),
#[error("Invalid command '{0}'\n{USAGE}")]
InvalidCommand(String),
#[error("Unrecognized arguments {0:?}\n{USAGE}")]
UnrecognizedArguments(Vec<OsString>),
}
pub enum Command {
Parse { path: PathBuf },
Run { path: PathBuf },
Repl,
}
impl Command {
pub fn from_environment() -> Result<Option<Self>, Error> {
let mut args = Arguments::from_env();
if args.contains(["-h", "--help"]) {
return Ok(None);
}
let cmd = match args.subcommand()?.as_deref() {
Some("parse") => {
let path = args.free_from_str()?;
Command::Parse { path }
}
Some("run") => {
let path = args.free_from_str()?;
Command::Run { path }
}
Some("repl") | None => Command::Repl,
Some(cmd) => return Err(Error::InvalidCommand(cmd.to_string())),
};
let remaining = args.finish();
if remaining.is_empty() {
Ok(Some(cmd))
} else {
Err(Error::UnrecognizedArguments(remaining))
}
}
}

197
src/evaluate.rs Normal file
View File

@ -0,0 +1,197 @@
use std::{collections::HashMap, fmt::Display};
use thiserror::Error;
use crate::builtins::{self, Builtin};
use crate::syntax::{Block, Expression, InteractiveEntry, Item, Name, Program};
pub type Dictionary<T> = HashMap<Name, T>;
#[derive(Debug, Clone)]
pub enum Value {
Builtin(Builtin),
Closure {
environment: Dictionary<Value>,
parameters: Vec<Name>,
result: Expression,
},
Boolean(bool),
Integer(i64),
Unit,
}
impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Builtin(b) => write!(f, "<builtin:{b:?}>"),
Value::Closure { parameters, .. } => write!(f, "<closure({})>", parameters.join(", ")),
Value::Boolean(b) => write!(f, "{b}"),
Value::Integer(i) => write!(f, "{i}"),
Value::Unit => write!(f, "()"),
}
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("The name '{0} was not bound to any value in this context")]
UnboundName(Name),
#[error("Tried to call a term which was not a function")]
NotAFunction,
#[error("Mismatched number of function parameters and arguments: expected {0}, got {1}")]
ArgumentsMismatch(usize, usize),
#[error("No function called main to run")]
MissingMainFunction,
#[error("Problem with builtin call: {0}")]
BuiltinError(#[from] builtins::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub struct Interpreter {
environment: Dictionary<Value>,
}
impl Interpreter {
pub fn new() -> Self {
Interpreter {
environment: Builtin::dictionary(),
}
}
fn nested(environment: Dictionary<Value>) -> Self {
Interpreter { environment }
}
pub fn run(&mut self, program: &Program) -> Result<Value> {
for item in program {
self.interpret_item(item)?;
}
if let Some(main) = self.environment.get("main").cloned() {
self.apply_call(&main, &vec![])
} else {
Err(Error::MissingMainFunction)
}
}
pub fn interpret_item(&mut self, item: &Item) -> Result<Value> {
match item {
Item::Fn {
name,
parameters,
body,
} => {
let closure = Value::Closure {
environment: self.environment.clone(),
parameters: parameters.clone(),
result: Expression::Block(Box::new(body.clone())),
};
self.environment.insert(name.clone(), closure.clone());
Ok(closure)
}
}
}
pub fn evaluate_interactive_entry(&mut self, entry: &InteractiveEntry) -> Result<Value> {
match entry {
InteractiveEntry::Item(item) => self.interpret_item(item),
InteractiveEntry::Binding(binding) => {
self.bind(binding.name.clone(), &binding.expression)
}
InteractiveEntry::Expression(term) => self.evaluate(term),
}
}
pub fn evaluate(&mut self, term: &Expression) -> Result<Value> {
match term {
Expression::Variable(name) => self
.environment
.get(name)
.ok_or(Error::UnboundName(name.clone()))
.cloned(),
Expression::Block(b) => self.evaluate_block(b.as_ref()),
Expression::Lambda { parameters, result } => Ok(Value::Closure {
environment: self.environment.clone(),
parameters: parameters.clone(),
result: *result.clone(),
}),
Expression::Call { callee, arguments } => {
let callee = self.evaluate(callee.as_ref())?;
self.apply_call(&callee, arguments)
}
Expression::Boolean(b) => Ok(Value::Boolean(*b)),
Expression::Integer(i) => Ok(Value::Integer(*i)),
Expression::Unit => Ok(Value::Unit),
}
}
fn apply_call(&mut self, callee: &Value, arguments: &Vec<Expression>) -> Result<Value> {
match callee {
Value::Closure {
environment,
parameters,
result,
} => {
if parameters.len() != arguments.len() {
return Err(Error::ArgumentsMismatch(parameters.len(), arguments.len()));
}
let mut nested = Interpreter::nested(environment.clone());
for (name, argument) in parameters.iter().zip(arguments) {
// we don't want arguments to refer to each other, so use the parent interpreter
self.bind_nested(name.clone(), argument, &mut nested)?;
}
nested.evaluate(result)
}
Value::Builtin(b) => {
if arguments.len() < b.min_parameters() {
return Err(Error::ArgumentsMismatch(
b.min_parameters(),
arguments.len(),
));
} else if let Some(max) = b.max_parameters() {
if arguments.len() > max {
return Err(Error::ArgumentsMismatch(max, arguments.len()));
}
}
let arguments = arguments
.iter()
.map(|term| self.evaluate(term))
.collect::<Result<Vec<_>>>()?;
b.call(arguments).map_err(Error::BuiltinError)
}
_ => Err(Error::NotAFunction),
}
}
fn evaluate_block(&mut self, block: &Block) -> Result<Value> {
let mut nested = self.clone();
for binding in &block.bindings {
nested.bind(binding.name.clone(), &binding.expression)?;
}
nested.evaluate(&block.result)
}
fn bind(&mut self, name: Name, term: &Expression) -> Result<Value> {
let value = self.evaluate(term)?;
self.environment.insert(name, value.clone());
Ok(value)
}
fn bind_nested(
&mut self,
name: Name,
term: &Expression,
nested: &mut Interpreter,
) -> Result<Value> {
let value = self.evaluate(term)?;
nested.environment.insert(name, value.clone());
Ok(value)
}
}
impl Default for Interpreter {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,150 +0,0 @@
use std::collections::HashMap;
use crate::syntax::{Expression, Name, Program, Statement};
pub type Environment = HashMap<Name, Value>;
#[derive(Debug, Clone)]
pub enum Value {
Boolean(bool),
Integer(i64),
Closure(Vec<Name>, Expression, Environment),
}
pub struct Interpreter {
environment: Environment,
}
#[derive(Debug)]
pub enum InterpreterError {
VariableNotFound(Name),
IncorrectArgumentCount(usize, usize),
NonFunctionCall(Value),
EmptyProgram,
ExecutionError(String),
}
impl From<String> for InterpreterError {
fn from(s: String) -> Self {
InterpreterError::ExecutionError(s)
}
}
impl std::fmt::Display for InterpreterError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
InterpreterError::VariableNotFound(name) => write!(f, "Variable '{}' not found", name),
InterpreterError::IncorrectArgumentCount(expected, actual) => {
write!(f, "Expected {} arguments, but got {}", expected, actual)
}
InterpreterError::NonFunctionCall(value) => {
write!(f, "Function call on non-function value: {:?}", value)
}
InterpreterError::EmptyProgram => write!(f, "Program is empty"),
InterpreterError::ExecutionError(s) => write!(f, "Execution error: {}", s),
}
}
}
type Result<T> = std::result::Result<T, InterpreterError>;
impl Interpreter {
pub fn new() -> Self {
Self {
environment: Environment::new(),
}
}
pub fn interpret_statement(&mut self, statement: &Statement) -> Result<Value> {
match statement {
Statement::Binding(name, expression) => {
let value = self.evaluate(expression)?;
self.environment.insert(name.clone(), value.clone());
Ok(value)
}
Statement::Expression(expression) => self.evaluate(expression),
}
}
pub fn interpret(&mut self, program: &Program) -> Result<Value> {
let mut result = None;
for statement in program {
result = Some(self.interpret_statement(statement)?);
}
match result {
Some(value) => Ok(value),
None => Err(InterpreterError::EmptyProgram),
}
}
fn evaluate(&mut self, expression: &Expression) -> Result<Value> {
match expression {
Expression::Variable(name) => self
.environment
.get(name)
.cloned()
.ok_or_else(|| InterpreterError::VariableNotFound(name.clone())),
Expression::Boolean(value) => Ok(Value::Boolean(*value)),
Expression::Integer(value) => Ok(Value::Integer(*value)),
Expression::Lambda(params, body) => Ok(Value::Closure(
params.clone(),
*body.clone(),
self.environment.clone(),
)),
Expression::Call(function, args) => {
let function_value = self.evaluate(function)?;
match function_value {
Value::Closure(params, body, mut environment) => {
if params.len() != args.len() {
return Err(InterpreterError::IncorrectArgumentCount(
params.len(),
args.len(),
));
}
let mut arg_values = Vec::new();
for arg in args {
arg_values.push(self.evaluate(arg)?);
}
// Bind argument values to parameter names in a new environment
for (param, arg_value) in params.iter().zip(arg_values.into_iter()) {
environment.insert(param.clone(), arg_value);
}
// Evaluate the body expression in the new environment
let mut interpreter = Interpreter { environment };
interpreter.evaluate(&body)
}
_ => Err(InterpreterError::NonFunctionCall(function_value)),
}
}
Expression::Block(statements, expression) => {
let new_environment = self.environment.clone();
for statement in statements {
self.interpret_statement(statement)?;
}
let mut interpreter = Interpreter {
environment: new_environment,
};
interpreter.evaluate(expression)
}
}
}
}
impl Default for Interpreter {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,5 +1,5 @@
use lalrpop_util::lalrpop_mod;
pub mod interpreter;
lalrpop_mod!(pub parser);
pub mod builtins;
pub mod cli;
pub mod evaluate;
pub mod parse;
pub mod syntax;

View File

@ -1,68 +1,69 @@
use std::env::args;
use std::fs::read_to_string;
use std::io::{stdin, stdout, BufRead, Write};
use std::path::Path;
use std::process::exit;
use mul::interpreter::Interpreter;
use mul::parser::{ProgramParser, StatementParser};
use anyhow::{anyhow, Context, Result};
fn repl() {
use mul::cli::{Command, USAGE};
use mul::evaluate::Interpreter;
use mul::parse::{parse_interactive_entry, parse_program};
use mul::syntax::Program;
fn main() -> Result<()> {
let Some(command) = Command::from_environment()? else {
eprintln!("mul\n{USAGE}");
return Ok(())
};
match command {
Command::Parse { path } => {
let program = parse(path.as_path())?;
println!("{program:#?}");
Ok(())
}
Command::Run { path } => {
let program = parse(path.as_path())?;
let mut interpreter = Interpreter::new();
let _result = interpreter.run(&program)?;
Ok(())
}
Command::Repl => repl(),
}
}
fn parse(path: &Path) -> Result<Program> {
let source =
read_to_string(path).with_context(|| format!("failed to open {}", path.display()))?;
parse_program(source.as_str())
.map_err(|e| anyhow!("{e}"))
.with_context(|| format!("failed to parse {}", path.display()))
}
fn repl() -> Result<()> {
let mut stdin = stdin().lock();
let mut stdout = stdout().lock();
let parser = StatementParser::new();
let mut intepreter = Interpreter::new();
let mut interpreter = Interpreter::new();
println!("welcome to M U L");
loop {
let mut line = String::new();
print!("> ");
stdout.flush().unwrap();
stdout.flush()?;
stdin
.read_line(&mut line)
.expect("Failed to read from stdin");
.context("Failed to read from stdin")?;
if line.trim().is_empty() {
println!();
continue;
}
if !line.trim_end().ends_with(';') {
line.push(';');
}
let parsed = match parser.parse(&line) {
let parsed = match parse_interactive_entry(&line) {
Ok(parsed) => parsed,
Err(lalrpop_util::ParseError::UnrecognizedEOF { .. }) => break,
Err(e) => {
eprintln!("{e}");
continue;
}
};
match intepreter.interpret_statement(&parsed) {
Ok(value) => println!("{value:?}"),
Err(lalrpop_util::ParseError::UnrecognizedEof { .. }) => break,
Err(e) => {
eprintln!("{e}");
continue;
}
};
let value = interpreter.evaluate_interactive_entry(&parsed)?;
println!("{value}");
}
}
fn build(file: &Path) {
let content = read_to_string(file).expect("Failed to read file");
let parser = ProgramParser::new();
let mut interpreter = Interpreter::new();
let program = parser.parse(content.as_str()).unwrap_or_else(|e| {
eprintln!("{e}");
exit(1);
});
let v = interpreter.interpret(&program).unwrap_or_else(|e| {
eprintln!("{e}");
exit(1);
});
println!("{v:?}")
}
fn main() {
if let Some(file) = args().nth(1) {
build(file.as_ref())
} else {
repl()
}
Ok(())
}

15
src/parse.rs Normal file
View File

@ -0,0 +1,15 @@
use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError};
use crate::syntax::{InteractiveEntry, Program};
lalrpop_mod!(parser, "/parse/parser.rs");
type Result<'input, T> = std::result::Result<T, ParseError<usize, Token<'input>, &'static str>>;
pub fn parse_program(input: &str) -> Result<Program> {
parser::ProgramParser::new().parse(input)
}
pub fn parse_interactive_entry(input: &str) -> Result<InteractiveEntry> {
parser::InteractiveEntryParser::new().parse(input)
}

72
src/parse/parser.lalrpop Normal file
View File

@ -0,0 +1,72 @@
use crate::syntax::{
Binding,
Block,
Expression,
Item,
InteractiveEntry,
Name,
Program,
};
grammar;
pub Program: Program = Item*;
pub InteractiveEntry: InteractiveEntry = {
Item => InteractiveEntry::Item(<>),
<Binding> ";"? => InteractiveEntry::Binding(<>),
<Expression> ";"? => InteractiveEntry::Expression(<>),
}
Item: Item = {
"fn" <name:Name> "(" <parameters:Comma<Name>> ")" <body:Block> ";"? => Item::Fn { <> },
}
Binding: Binding =
"let" <name:Name> "=" <expression:Expression> => Binding { <> };
Expression: Expression = {
Simple,
Boxed<Block> => Expression::Block(<>),
};
Simple: Expression = {
"|" <parameters:Comma<Name>> "|" <result:Boxed<Expression>> => Expression::Lambda { <> },
Atom,
};
Atom: Expression = {
Name => Expression::Variable(<>),
<callee:Boxed<Atom>> "(" <arguments:Comma<Expression>> ")" => Expression::Call { <> },
Integer => Expression::Integer(<>),
Boolean => Expression::Boolean(<>),
"(" <Expression> ")",
"(" ")" => Expression::Unit,
};
BlockBinding = <Binding> ";";
Block: Block =
"{" <bindings:BlockBinding*> <result:Expression> "}" => Block { <> };
// TODO: decide on identifier syntax
Name: Name = r"[a-zA-Z_]+" => <>.to_string();
Integer: i64 = r"[0-9]+" => <>.parse::<i64>().unwrap();
Boolean: bool = {
"true" => true,
"false" => false,
}
Boxed<T>: Box<T> = T => Box::new(<>);
Comma<T>: Vec<T> = {
<mut v:(<T> ",")*> <e:T?> => match e {
None => v,
Some(e) => {
v.push(e);
v
}
}
};

View File

@ -1,52 +0,0 @@
use crate::syntax::{Expression, Name, Program, Statement};
grammar;
pub Program: Program = Statement*;
pub Statement: Statement = {
"let" <Name> "=" <Expression> ";" => Statement::Binding(<>),
<Block> ";"? => Statement::Expression(<>),
<Simple> ";" => Statement::Expression(<>),
};
pub Expression = {
Simple,
Block,
};
Simple: Expression = {
"|" <parameters:Comma<Name>> "|" <body:Expression> => Expression::Lambda(parameters, Box::new(body)),
Atom,
};
Atom: Expression = {
Name => Expression::Variable(<>),
<callee:Atom> "(" <arguments:Comma<Expression>> ")" => Expression::Call(Box::new(callee), arguments),
Integer => Expression::Integer(<>),
Boolean => Expression::Boolean(<>),
"(" <Expression> ")",
};
Block: Expression =
"{" <statements:Statement*> <result:Expression> "}" => Expression::Block(statements, Box::new(result));
// TODO: decide on identifier syntax
Name: Name = r"[a-zA-Z_]+" => <>.to_string();
Integer: i64 = r"[0-9]+" => <>.parse::<i64>().unwrap();
Boolean: bool = {
"true" => true,
"false" => false,
}
Comma<T>: Vec<T> = {
<mut v:(<T> ",")*> <e:T?> => match e {
None => v,
Some(e) => {
v.push(e);
v
}
}
};

View File

@ -1,19 +1,57 @@
//! Syntax elements.
/// Identifiers.
pub type Name = String;
pub type Program = Vec<Statement>;
/// The rough equivalent of a module.
pub type Program = Vec<Item>;
/// A fragment of code entered in, e.g., the REPL.
#[derive(Debug, Clone)]
pub enum Statement {
Binding(Name, Expression),
pub enum InteractiveEntry {
Item(Item),
Binding(Binding),
Expression(Expression),
}
/// A top level definition.
#[derive(Debug, Clone)]
pub enum Item {
Fn {
name: Name,
parameters: Vec<Name>,
body: Block,
},
}
/// Syntactic mapping from an identifier to an expression.
#[derive(Debug, Clone)]
pub struct Binding {
pub name: Name,
pub expression: Expression,
}
/// Some individually addressable syntax node, either atomic or compound.
#[derive(Debug, Clone)]
pub enum Expression {
Variable(Name),
Block(Vec<Statement>, Box<Expression>),
Lambda(Vec<Name>, Box<Expression>),
Call(Box<Expression>, Vec<Expression>),
Block(Box<Block>),
Lambda {
parameters: Vec<Name>,
result: Box<Expression>,
},
Call {
callee: Box<Expression>,
arguments: Vec<Expression>,
},
Boolean(bool),
Integer(i64),
Unit,
}
/// A sequence of bindings with a final result.
#[derive(Debug, Clone)]
pub struct Block {
pub bindings: Vec<Binding>,
pub result: Expression,
}