Compare commits
10
Commits
fab8c7c3a8
...
f8ad49b7d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ad49b7d5 | ||
|
|
72d9fa7f88 | ||
|
|
24e94b8a92 | ||
|
|
5a46db89d6 | ||
|
|
7773c79d6f | ||
|
|
7a8ef1a485 | ||
|
|
d7bcac7092 | ||
|
|
c4eca9defe | ||
|
|
f22e916e0c | ||
|
|
45455d3a02 |
No files matched your search
+108
@@ -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.
|
||||||
@@ -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
|
To combine more than two evaluators, I created a `pipeline` macro that
|
||||||
constructs nested compositions of evaluators with Rust's `macro_rules`
|
constructs nested compositions of evaluators with Rust's `macro_rules`
|
||||||
system.
|
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.
|
||||||
@@ -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.
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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
@@ -3,12 +3,16 @@ pub mod const_fold;
|
|||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod differentiate;
|
pub mod differentiate;
|
||||||
pub mod simplify;
|
pub mod simplify;
|
||||||
|
pub mod canonicalise;
|
||||||
|
|
||||||
|
use crate::expression::Expression;
|
||||||
|
|
||||||
pub use normalise::*;
|
pub use normalise::*;
|
||||||
pub use const_fold::*;
|
pub use const_fold::*;
|
||||||
pub use compose::*;
|
pub use compose::*;
|
||||||
pub use differentiate::*;
|
pub use differentiate::*;
|
||||||
pub use simplify::*;
|
pub use simplify::*;
|
||||||
|
pub use canonicalise::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -21,3 +25,22 @@ pub trait Evaluator {
|
|||||||
|
|
||||||
fn eval(&mut self, input: Self::Expr) -> Result<Self::Expr, Self::Error>;
|
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())
|
||||||
|
}
|
||||||
@@ -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
@@ -5,9 +5,7 @@ pub mod binding;
|
|||||||
pub mod read;
|
pub mod read;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod print;
|
pub mod print;
|
||||||
|
|
||||||
pub mod repl;
|
pub mod repl;
|
||||||
|
|
||||||
pub use expression::Expression;
|
|
||||||
pub use eval::Evaluator;
|
|
||||||
pub use repl::Repl;
|
pub use repl::Repl;
|
||||||
|
pub use eval::evaluator;
|
||||||
Reference in new issue
Block a user