Compare commits

...
10 Commits
Author SHA1 Message Date
cdo f8ad49b7d5 Add README 2026-09-26 15:45:39 +01:00
cdo 72d9fa7f88 Add license (Komorebi 2.0.0) 2026-09-26 15:45:35 +01:00
cdo 24e94b8a92 Rename demo binary to repl 2026-09-26 15:45:30 +01:00
cdo 5a46db89d6 Write closing thoughts in development log 2026-09-26 14:10:12 +01:00
cdo 7773c79d6f Update development log 2026-09-26 13:58:12 +01:00
cdo 7a8ef1a485 Reorganise root exports and update demo 2026-09-26 13:46:52 +01:00
cdo d7bcac7092 Wrap full evaluator pipeline into evaluator() function 2026-09-26 13:46:46 +01:00
cdo c4eca9defe Add canonicalisation pass 2026-09-26 13:32:41 +01:00
cdo f22e916e0c Use differentiation pipeline in demo 2026-09-26 13:25:05 +01:00
cdo 45455d3a02 Implement std::fmt::Display for eval::Error 2026-09-26 13:24:58 +01:00
8 changed files with 417 additions and 23 deletions

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
# Komorebi License
Version 2.0.0
## Acceptance
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
## Copyright License
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose. However, you may only distribute the source
code of the software according to the [Distribution License](
#distribution-license), you may only make changes according
to the [Changes License](#changes-license), and you may not
otherwise distribute the software or new works based on the
software.
## Distribution License
The licensor grants you an additional copyright license to
distribute copies of the source code of the software. Your
license to distribute covers distributing the source code of
the software with changes permitted by the [Changes License](
#changes-license).
## Changes License
The licensor grants you an additional copyright license to
make changes for any permitted purpose.
## Patent License
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Personal Uses
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Fair Use
You may have "fair use" rights for the software under the
law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have
violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.
+80
View File
@@ -193,3 +193,83 @@ the evaluators were run in sequence on any input expression.
To combine more than two evaluators, I created a `pipeline` macro that
constructs nested compositions of evaluators with Rust's `macro_rules`
system.
### Differentiator
The differentiation pass itself was very easy to implement given the
simplications made by first having a normalisation pass. I also had
decided to only support constant exponents, which simplifies things
too. In the end, it ended up being more-or-less a fairly direct
translation of the sum rule and product rule into code, along with the
power rule and chain rule combined for handling exponents.
### Simplification
The output of the differentiation pass was correct but contained a lot
of noise such as terms multiplied by zero and other similar things. I
therefore implemented a basic simplification pass that applied various
identities such as `x + 0 = x`, `0x = 0` and `1x = 1`. Expression
simplification is famously a very complex problem, but I kept this
pass quite limited in scope and only implemented the most high-payoff
features.
### Canonicalisation
To make the output a little easier to read, the final evaluation pass
I implemented was a limited multiplication reordering canonicalisation
pass that ensured that products had the constants first. This is
still incomplete as the re-ordering only takes place within binary
products (so `x*4` becomes `4x`, but `x*(y*4)` would become `x*4y` not
`4xy`), and terms in a sum were not re-ordered at all. However, I
decided full canonicalisation would be excessively complex for this
project and decided to leave it there, as the output was acceptably
readable at this point anyway.
### Wrapping up
The final pipeline I ended up with was:
1. Normalisation
2. Differentiation
3. Constant folding
4. Simplification
5. Constant folding again
6. Canonicalisation
I then created an `evaluator` function to wrap this up in the `eval`
module, to allow for a simple interface to the whole library.
## Conclusion / Closing thoughts
Creating the differentiation program was a lot of fun, and I'm happy
with the result I ended up with. The code is well-tested, and for the
most part is fairly maintainable (perhaps with the area needing the
most work in that respect being the constant folding pass).
In order to quantify how effective the test suite is, I ran a mutation
testing tool (`cargo-mutants`) and it found that only 3 out of 221
generated mutants were missed by the test suite, giving a mutation
score of 98.6% which is very good. The three missed mutants were also
limited to the demo program itself (which makes sense since it has no
tests) and error message formatting code, so none of them are
particularly concerning.
There are quite a lot of areas where further work could be done, such
as adding support for functions like sine, cosine and natural
logarithms; enhancing the simplification and canonicalisation passes
and allowing non-constant exponents. However, I think the most
interesting enhancement would be to add equalities to the language and
allow definitions to be stored within a session, with explicit
differentiation commands to get the deriviative of something, such as:
```
> y = x^3 - x^2
> dy/dx
3x^2 - 2x
```
This, along with a few other features, would transform the program
into a more general-purpose symbolic mathematics engine, which would
be very cool and useful. The current architecture would allow for
this quite nicely, as evaluators are able to have mutable state which
could be used for storing variable bindings.
+58
View File
@@ -0,0 +1,58 @@
# Deriv
Deriv is a basic, command-line symbolic differentiation engine,
supporting reasonably standard mathematical notation for input and
output.
An example interaction:
```
> 2x^2 + 3x
4x + 3
> x^3 - x^2
3x^2 + -2x
```
See [LOG.md](LOG.md) for a log of the development process.
## Building and Running
Deriv is written in Rust and uses Cargo as its build system. The Rust
toolchain (including Cargo) is most easily installed with
[Rustup](https://rustup.rs/).
Given that you have a toolchain installed, the REPL can be ran with:
```
cargo run --bin repl
```
Alternatively, to only build the project:
```
cargo build
```
To run the test suite:
```
cargo test
```
## Limitations
Deriv currently has several limitations:
- Only the variable "x" is currently supported.
- Exponents must be constant (so e.g. "x^x" is unsupported)
- Expression simplification is quite limited, many output expressions
may not be fully simplified
- There are no built-in constants like e or π
- Functions such as sine, cosine and natural logarithms are not
supported
- The input must be UTF-8 Basic Latin / ASCII-compatible text
## License
Deriv is Copyright © Camden Dixie O'Brien, and is available under the
Komorebi 2.0.0 license. See [LICENSE.md](LICENSE.md) for details.
-20
View File
@@ -1,20 +0,0 @@
use deriv::Expression;
use deriv::Evaluator;
use deriv::Repl;
struct DummyEvaluator;
impl Evaluator for DummyEvaluator {
type Expr = Expression;
type Error = std::io::Error;
fn eval(&mut self, input: Self::Expr) -> Result<Self::Expr, Self::Error> {
Ok(input)
}
}
fn main() -> std::io::Result<()> {
let mut repl = Repl::new(
std::io::stdin(), std::io::stdout(), DummyEvaluator {});
repl.run()
}
+7
View File
@@ -0,0 +1,7 @@
fn main() -> std::io::Result<()> {
let mut repl = deriv::Repl::new(
std::io::stdin(),
std::io::stdout(),
deriv::evaluator('x'));
repl.run()
}
+23
View File
@@ -3,12 +3,16 @@ pub mod const_fold;
pub mod compose;
pub mod differentiate;
pub mod simplify;
pub mod canonicalise;
use crate::expression::Expression;
pub use normalise::*;
pub use const_fold::*;
pub use compose::*;
pub use differentiate::*;
pub use simplify::*;
pub use canonicalise::*;
#[derive(Debug, PartialEq)]
pub enum Error {
@@ -21,3 +25,22 @@ pub trait Evaluator {
fn eval(&mut self, input: Self::Expr) -> Result<Self::Expr, Self::Error>;
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Error::UnsupportedExpression => "Unsupported expression",
})
}
}
pub fn evaluator(var: char)
-> impl Evaluator<Expr = Expression, Error = Error> {
crate::pipeline!(
Normaliser::new(),
Differentiator::new(var),
ConstFolder::new(),
Simplifier::new(),
ConstFolder::new(),
Canonicaliser::new())
}
+140
View File
@@ -0,0 +1,140 @@
use crate::expression::Expression;
use crate::eval::{Error, Evaluator};
pub struct Canonicaliser;
impl Canonicaliser {
pub fn new() -> Self {
Self {}
}
}
impl Evaluator for Canonicaliser {
type Expr = Expression;
type Error = Error;
fn eval(&mut self, input: Expression) -> Result<Expression, Error> {
Ok(canonicalise(input))
}
}
fn canonicalise(expr: Expression) -> Expression {
match expr {
Expression::Sum(x, y) => recurse(Expression::Sum, x, y),
Expression::Product(x, y) => recurse(product, x, y),
Expression::Exponent(x, y) => recurse(Expression::Exponent, x, y),
_ => expr
}
}
fn product(
x: std::boxed::Box<Expression>,
y: std::boxed::Box<Expression>)
-> Expression
{
if let Some(_) = y.integer() {
Expression::Product(y, x)
} else {
Expression::Product(x, y)
}
}
fn recurse(
f: impl Fn(
std::boxed::Box<Expression>,
std::boxed::Box<Expression>) -> Expression,
x: std::boxed::Box<Expression>,
y: std::boxed::Box<Expression>)
-> Expression
{
f(boxed(x), boxed(y))
}
fn boxed(
mut expr: std::boxed::Box<Expression>) -> std::boxed::Box<Expression>
{
*expr = canonicalise(*expr);
expr
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn product_integer_symbol_is_unchanged() {
let input = Expression::product(
Expression::Integer(1),
Expression::Symbol('x'));
let result = Canonicaliser::new().eval(input).unwrap();
assert_eq!(
result,
Expression::product(
Expression::Integer(1),
Expression::Symbol('x')));
}
#[test]
fn product_symbol_integer_becomes_product_integer_symbol() {
let input = Expression::product(
Expression::Symbol('x'),
Expression::Integer(1));
let result = Canonicaliser::new().eval(input).unwrap();
assert_eq!(
result,
Expression::product(
Expression::Integer(1),
Expression::Symbol('x')));
}
#[test]
fn product_symbol_integer_nested_in_sum_is_reordered() {
let input = Expression::sum(
Expression::Integer(1),
Expression::product(
Expression::Symbol('x'),
Expression::Integer(1)));
let result = Canonicaliser::new().eval(input).unwrap();
assert_eq!(
result,
Expression::sum(
Expression::Integer(1),
Expression::product(
Expression::Integer(1),
Expression::Symbol('x'))));
}
#[test]
fn product_symbol_integer_nested_in_product_is_reordered() {
let input = Expression::product(
Expression::Integer(1),
Expression::product(
Expression::Symbol('x'),
Expression::Integer(1)));
let result = Canonicaliser::new().eval(input).unwrap();
assert_eq!(
result,
Expression::product(
Expression::Integer(1),
Expression::product(
Expression::Integer(1),
Expression::Symbol('x'))));
}
#[test]
fn exponent_symbol_integer_nested_in_exponent_is_reordered() {
let input = Expression::exponent(
Expression::Integer(1),
Expression::product(
Expression::Symbol('x'),
Expression::Integer(1)));
let result = Canonicaliser::new().eval(input).unwrap();
assert_eq!(
result,
Expression::exponent(
Expression::Integer(1),
Expression::product(
Expression::Integer(1),
Expression::Symbol('x'))));
}
}
+1 -3
View File
@@ -5,9 +5,7 @@ pub mod binding;
pub mod read;
pub mod eval;
pub mod print;
pub mod repl;
pub use expression::Expression;
pub use eval::Evaluator;
pub use repl::Repl;
pub use eval::evaluator;