Compare commits

..

No commits in common. "bcee1cf16078fdcb3fda1a17f8f66c5614082e9a" and "3ce75cfed8fb30cee6934b9b61e357a171e700dd" have entirely different histories.

18 changed files with 347 additions and 717 deletions

48
Cargo.lock generated
View File

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

View File

@ -4,11 +4,9 @@ version = "0.1.0"
edition = "2021"
[build-dependencies]
lalrpop = "0.20.0"
lalrpop = "0.19.9"
[dependencies]
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"] }
anyhow = "1.0.71"
lalrpop-util = { version = "0.19.9", features = ["lexer"] }
regex = "1"
thiserror = "1.0.43"

View File

@ -1,9 +1,37 @@
# mul
## mat's untitled language
mat's untitled language
see [`example.mul`](/example.mul) for a syntax sample.
## goals
see [`GOALS.md`](/docs/GOALS.md) for guiding principles and inspirations.
### start simple and grow
see [`ROADMAP.md`](/docs/ROADMAP.md) for specific implementation plans and progress.
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.

View File

@ -1,61 +0,0 @@
# 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,9 +14,6 @@
- [x] integers
- [ ] floating point numbers
- [ ] text
- [ ] items
- [x] function definitions
- [ ] type definitions
- [ ] functions
- [x] lambdas / closures
- [ ] generic functions
@ -27,6 +24,5 @@
- [ ] sum types
- [ ] variant types
- [ ] pattern matching
- [ ] traits (?)
- [ ] traits
- [ ] operator overloading
- [ ] capabilities/effects

View File

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

View File

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

View File

@ -1,132 +0,0 @@
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)
}
}

View File

@ -1,60 +0,0 @@
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))
}
}
}

View File

@ -1,197 +0,0 @@
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()
}
}

150
src/interpreter.rs Normal file
View File

@ -0,0 +1,150 @@
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 @@
pub mod builtins;
pub mod cli;
pub mod evaluate;
pub mod parse;
use lalrpop_util::lalrpop_mod;
pub mod interpreter;
lalrpop_mod!(pub parser);
pub mod syntax;

View File

@ -1,69 +1,68 @@
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 anyhow::{anyhow, Context, Result};
use mul::interpreter::Interpreter;
use mul::parser::{ProgramParser, StatementParser};
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<()> {
fn repl() {
let mut stdin = stdin().lock();
let mut stdout = stdout().lock();
let mut interpreter = Interpreter::new();
println!("welcome to M U L");
let parser = StatementParser::new();
let mut intepreter = Interpreter::new();
loop {
let mut line = String::new();
print!("> ");
stdout.flush()?;
stdout.flush().unwrap();
stdin
.read_line(&mut line)
.context("Failed to read from stdin")?;
.expect("Failed to read from stdin");
if line.trim().is_empty() {
println!();
continue;
}
let parsed = match parse_interactive_entry(&line) {
if !line.trim_end().ends_with(';') {
line.push(';');
}
let parsed = match parser.parse(&line) {
Ok(parsed) => parsed,
Err(lalrpop_util::ParseError::UnrecognizedEof { .. }) => break,
Err(lalrpop_util::ParseError::UnrecognizedEOF { .. }) => break,
Err(e) => {
eprintln!("{e}");
continue;
}
};
match intepreter.interpret_statement(&parsed) {
Ok(value) => println!("{value:?}"),
Err(e) => {
eprintln!("{e}");
continue;
}
};
let value = interpreter.evaluate_interactive_entry(&parsed)?;
println!("{value}");
}
Ok(())
}
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()
}
}

View File

@ -1,15 +0,0 @@
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)
}

View File

@ -1,72 +0,0 @@
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
}
}
};

52
src/parser.lalrpop Normal file
View File

@ -0,0 +1,52 @@
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,57 +1,19 @@
//! Syntax elements.
/// Identifiers.
pub type Name = String;
/// The rough equivalent of a module.
pub type Program = Vec<Item>;
pub type Program = Vec<Statement>;
/// A fragment of code entered in, e.g., the REPL.
#[derive(Debug, Clone)]
pub enum InteractiveEntry {
Item(Item),
Binding(Binding),
pub enum Statement {
Binding(Name, Expression),
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(Box<Block>),
Lambda {
parameters: Vec<Name>,
result: Box<Expression>,
},
Call {
callee: Box<Expression>,
arguments: Vec<Expression>,
},
Block(Vec<Statement>, Box<Expression>),
Lambda(Vec<Name>, Box<Expression>),
Call(Box<Expression>, 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,
}