132 lines
5.5 KiB
Markdown
132 lines
5.5 KiB
Markdown
# Development Log
|
|
|
|
## Initial Project Idea and Planning
|
|
|
|
I decided to make a basic symbolic differentiation engine. The
|
|
essence of this is that you can provide mathematical expressions as
|
|
input, and the program can compute the derivative of them. For the
|
|
sake of simplicity, it will be command-line based, so its input and
|
|
output will both be text. An example interation might look something
|
|
like:
|
|
|
|
```
|
|
> 2x^2 + 3x
|
|
4x + 3
|
|
> x^3 - x^2
|
|
3x^2 - 2x
|
|
```
|
|
|
|
### Architecture
|
|
|
|
The program will have three major components: the reader, which will
|
|
take user input and parse it into a convenient internal representation
|
|
(generally an AST of some kind); the solver, which will transform the
|
|
input expression into an output expression; and the printer which will
|
|
format output expressions back to human-readable text. This is
|
|
essentially a standard architecture for a simple interpreter.
|
|
|
|

|
|
|
|
The reader will be composed of two passes: a simple state machine
|
|
lexer to tokenise user input and a parser to build an AST from the
|
|
tokens. For simplicity here, I will only be supporting basic latin
|
|
characters in UTF-8 (i.e. ASCII-compatible text).
|
|
|
|
Since I am only implementing differentiation, the solver can be
|
|
relatively simple, consisting of a pattern-matching loop with an
|
|
accompanying ruleset.
|
|
|
|
The printer is the simplest of the three components and will consist
|
|
only of a procedure to walk an AST and emit a serialisation of it
|
|
equivalent to the input language.
|
|
|
|
In addition to the major components, a harness will be needed to tie
|
|
them together and handle errors. This will be in the form of a simple
|
|
REPL (Read Eval Print Loop).
|
|
|
|
### Implementation and Testing Strategy
|
|
|
|
I'll begin by implementing the reader and printer, as this will allow
|
|
me to get to a working end-to-end demo as quickly as possible by
|
|
initially skipping the solver and simply printing the input
|
|
expressions unmodified. This will then enable easy and fast iteration
|
|
when implementing the solver logic.
|
|
|
|
I will be writing a suite of unit tests to accompany every module of
|
|
the system, following a TDD (Test-Driven Development) methodology.
|
|
This system is quite simple and well-defined, so the advantage of TDD
|
|
(as opposed to writing tests after-the-fact) is probably relatively
|
|
minimal here, especially since it will not be being modified or
|
|
maintained after its initial development and will not be being worked
|
|
on by multiple people, but the practise will be beneficial regardless
|
|
in tightening the feedback loop of the development process and
|
|
ensuring that the code is structured to be amenable to testing. If I
|
|
have time I'll also implement some automated integration tests to
|
|
verify end-to-end functionality.
|
|
|
|
I'll be using Rust as the implementation language, as I am fairly
|
|
familiar with it and it is well-suited to such tasks. In addition,
|
|
its strong type system and borrow checker are useful in catching many
|
|
classes of bugs at compile time and it has unit testing support
|
|
provided in the base language, which is convenient for doing TDD.
|
|
|
|
### LLM / AI Usage
|
|
|
|
I will not be using any LLM coding assistants for this project.
|
|
|
|
## Reader
|
|
|
|
### Lexer
|
|
|
|
I first implemented the lexer with a very simple finite state machine
|
|
consisting of two states, `Init` and `Integer`. This was
|
|
straightforward as the input language is very simple.
|
|
|
|
### Parser
|
|
|
|
Since the input grammar consists of terms, parentheses and infix
|
|
operators with varying precedence, Pratt parsing is the logical choice
|
|
for the implementation of a parser.
|
|
|
|
I find the usual way of handling associativity in precedence climbing
|
|
parsers (wherein a small amount is added to the binding power on one
|
|
side) somewhat inelegant, so I decided to implement a simple algebra
|
|
for binding powers that handles associativity using custom comparison
|
|
semantics.
|
|
|
|
Implementing the parser itself was straightforward as I have
|
|
implemented Pratt parsing many times before (though not in Rust
|
|
specifically).
|
|
|
|
To tie together the lexer and parser, and to provide a convenient
|
|
means of decoupling other components from them for testing purposes, I
|
|
then implemented the Rust standard library's `FromStr` trait, which is
|
|
for types that can be parsed from a string.
|
|
|
|
## Printer
|
|
|
|
For expression printing, a simple recursive tree-walking algorithm,
|
|
dispatching on the various kinds of expression was sufficient.
|
|
Satisfyingly, I found I was able to use the same binding power algebra
|
|
I defined for the parser in deciding when to use parentheses, and thus
|
|
got correct associativity handling effectively for free, while also
|
|
keeping all associativity-handling logic well-factored in one place.
|
|
|
|
The only complication was in the handling of products, as here there
|
|
was some ambiguity as to when to use explicit or implicit
|
|
multiplication (that is `x*y` or `xy`). Products of symbols with
|
|
other symbols and integers with symbols are more naturally expressed
|
|
with implicit multiplication, but of course this couldn't be used for
|
|
products of integers with other integers as this would result in
|
|
incorrect serialisations (such as "12" for the product of 1 and 2). I
|
|
ultimately settled on an approach where the right-hand operand is
|
|
first printed into a string, and an explicit multiplication sign is
|
|
used whenever that serialisation begins with a numerical character
|
|
(either a digit or a minus sign). This nicely handled all of the
|
|
cases I could think of and systematically avoids any erroneous
|
|
serialisations.
|
|
|
|
The printing logic could then be packaged up in an implementation of
|
|
the Rust standard library's `Display` trait, which is for types that
|
|
have a textual representation.
|