Table of Contents
- Quick Overview
- About Our Output
- Test Cases
- Possible Expansion?
- Github Setup
This project contains the simplest possible compiler, which converts mathematical operations into assembly language, allowing all the speed in your sums!
Because this is a simple project it provides only a small number of primitives:
^- Raise to a power
- Stack operations:
swap- Swap the top-two items on the stack
dup- Duplicate the topmost stack-entry.
- Built-in constants:
Despite this toy-functionality there is a lot going on, and we support:
- Full RPN input
- Floating-point numbers (i.e. one-third multipled by nine is 3)
1 3 / 9 *
- Negative numbers work as you'd expect.
Some errors will be caught at run-time, as the generated code has support for:
- Detecting, and preventing, division by zero.
- Detecting insufficient arguments being present upon the stack.
- For example this program is invalid
3 +, because the addition operator requires two operands. (i.e.
3 4 +)
- For example this program is invalid
There are two ways to install this project from source, which depend on the version of the go version you're using.
If you just need the binaries you can find them upon the project release page.
Source Installation go <= 1.11
If you're using
go before 1.11 then the following command should fetch/update
overseer, and install it upon your system:
$ go get -u github.com/skx/math-compiler
Source installation go >= 1.12
If you're using a more recent version of
go (which is highly recommended), you need to clone to a directory which is not present upon your
git clone https://github.com/skx/math-compiler cd math-compiler go install
The intention of this project is mostly to say "I wrote a compiler", because I've already experimented with a language, and implemented a BASIC. The things learned from those projects were pretty useful, even if the actual results were not so obviously useful in themselves.
Because there are no shortages of toy-languages, and there is a lot of complexity in writing another for no real gain, I decided to just focus upon a simple core:
- Allowing "maths stuff" to be "compiled".
In theory this would allow me to compile things like this:
2 + ( 4 * 54 )
However I even simplified that, via the use of a "Reverse Polish" notation, so if you want to run that example you'd enter the expression as:
4 54 * 2 +
About Our Output
The output of
math-compiler will typically be an assembly-language file, which then needs to be compiled before it may be executed.
Given our previous example of
2 + ( 4 * 54) we can compile & execute that program like so:
$ math-compiler '4 54 * 2+' > sample.s $ gcc -static -o sample ./sample.s $ ./sample Result 218
There you see:
math-compilerwas invoked, and the output written to the file
gccwas used to assemble
sample.sinto the binary
- The actual binary was then executed, which showed the result of the calculation.
If you prefer you can also let the compiler do the heavy-lifting, and generate an executable for you directly. Simply add
-compile, and execute the generated
$ math-compiler -compile=true '2 8 ^' $ ./a.out Result 256
Or to compile and execute directly:
$ math-compiler -run '3 45 * 9 + 12 /' Result 12
The codebase itself contains some simple test-cases, however these are not comprehensive as a large part of our operation is merely to populate a simple template-file, and it is hard to test that.
To execute the integrated tests use the standard go approach:
$ go test [-race] ./...
In addition to the internal test cases there are also some functional tests contained in test.sh - these perform some calculations and verify they produce the correct result.
frodo ~/go/src/github.com/skx/math-compiler $ ./test.sh ... Expected output found for '2 0 ^'  Expected output found for '2 1 ^'  Expected output found for '2 2 ^'  Expected output found for '2 3 ^'  Expected output found for '2 4 ^'  Expected output found for '2 5 ^'  ... Expected output found for '2 30 ^'  ...
Debugging the generated programs
If you run the compiler with the
-debug flag a breakpoint will be generated
immediately at the start of the program. You can use that breakpoint to easily
debug the generated binary via
For example you might generate a program "
2 3 + 4 /" like so:
$ math-compiler -compile -debug '2 3 + 4 /'
Now you can launch that binary under
gdb, and run it:
$ gdb ./a.out (gdb) run .. Program received signal SIGTRAP, Trace/breakpoint trap. 0x00000000006b20cd in main ()
Dissassemble the code via
disassemble, and step over instructions one at a time via
stepi. If your program is long you might see a lot of output from the
(gdb) disassemble Dump of assembler code for function main: 0x00000000006b20cb: push %rbp 0x00000000006b20cc: int3 => 0x00000000006b20cd: fldl 0x6b20b3 0x00000000006b20d4: fstpl 0x6b2090 0x00000000006b20db: mov 0x6b2090,%rax 0x00000000006b20e3: push %rax 0x00000000006b20e4: fldl 0x6b20bb 0x00000000006b20eb: fstpl 0x6b2090 0x00000000006b20f2: mov 0x6b2090,%rax 0x00000000006b20fa: push %rax ... ...
You can set a breakpoint at a line in the future, and continue running till you hit it, with something like this:
(gdb) break *0x00000000006b20fa (gdb) cont
Once there inspect the registers with commands like these two:
(gdb) print $rax (gdb) info registers
My favourite is
info registers float, which shows you the floating-point
values as well as the raw values:
(gdb) info registers float st0 0.140652076786443369638 (raw 0x3ffc90071917a6263000) st1 0 (raw 0x00000000000000000000) st2 0 (raw 0x00000000000000000000) ... ...
Further documentation can be found in the
gdb manual, which is worth reading
if you've an interest in compilers, debuggers, and decompilers.
The obvious thing to improve in this compiler is to add support for more operations. At the moment support for the most obvious/common operations is present, but perhaps more functions could be added.
This repository is configured to run tests upon every commit, and when pull-requests are created/updated. The testing is carried out via .github/run-tests.sh which is used by the github-action-tester action.