Compare commits
10
Commits
| 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
|
||||
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.
|
||||
@@ -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 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())
|
||||
}
|
||||
@@ -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 eval;
|
||||
pub mod print;
|
||||
|
||||
pub mod repl;
|
||||
|
||||
pub use expression::Expression;
|
||||
pub use eval::Evaluator;
|
||||
pub use repl::Repl;
|
||||
pub use eval::evaluator;
|
||||
Reference in new issue
Block a user