Make your own minicompiler (aka parser) on the Web in Plain English (Calculator with C++)
Tags |
---|
Youâre going to learn to make your own compiler on the Web, indeed C++ and WebAssembly are the bread and butter of your needs, and unsurprisingly JavaScript, HTML, and CSS too.
Your chiefly goal is a calculator that evaluates ordinary expressions you type in âcause you want to go into shape and acquaintance. An outward step career is writing a simple calculator; it shed light on programming techniques, compilers, and the theory of computation. By the way, my muse is Programming -- Principles and Practice Using C++ by Stroustrup. So, letâs do it.
Please check out the dependencies on GitHub.
Use cases
Since user experience is first and given users want a buttonless calculator, your calculator gotta receive infix expressions and return the result, but which expressions are your concern? Operators your calculator admitted are ï»ż on usual operator precedence.
If you enter | the program should respond |
2+3.1*4 | 14.4 |
2+4/2 | 4 |
2+3%3 | 2 |
10**2%5 | 0 |
(25+5)**2/2%5 | 0 |
Local server and production hosting service
Please checkout dependencies on GitHub.
Iâve chosen Firebase hosting since Iâm able to set up wasm files (WebAssembly compiled files) easier than other services such as Vercel or Netlfy, but you can deploy the project to them. We donât really do more than it.
Whatever hosting service you choose, you should set up wasm file headers such that the server responds them as application/wasm, it must be not plain text format or another one.
"headers": [
{
"source": "**/*.@(wasm)",
"headers": [
{
"key": "Content-Type",
"value": "application/wasm"
}
]
}
]
In your localhost, use make start
it starts a Firebase local server with previous settings.
Download nanolib and compile your calculator.cpp
Your project needs nanolib
since Clang is enough for you. Compile the simplest file to work. Checkout this empty commit.
#include "nanolibc/libc.h"
#include "nanolibc/libc_extra.h"
#define WASM_EXPORT __attribute__((visibility("default"))) extern "C"
WASM_EXPORT char* allocateMemoryForString(int size) {
return new char[size];
}
WASM_EXPORT void freeMemoryForString(char* str) {
delete[] str;
}
WASM_EXPORT double calculate(const char* expression) {
return 0;
}
Compile your project with make
make
Since Makefile is not a chief concern you must be ignored it.
Start localhost
make start
User interface and scripts
I wonât talk deeply about Tailwind. If you would do a simple form, youâre OK.
<form>
<input id="calculator">
</form>
<span id="result" ></span>
But you must link form and your calculator; it should call your C++ code when the users changes their input.
<script type="module">
document.getElementById("calculator").addEventListener('input', function (evt) {
// This call your C++ code.
});
</script>
And, of course, you want to instantiate your wasm file.
const calculator = await (async (url) => {
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), importObject);
return instance;
})("calculator.wasm");
Bad news! The C++ WASM module has its own memory and it only accepts numbers, but you need to share the memory with JavaScript and pass an expression to your function. The good news is that JavaScript's standard, built-in objects included an encoder, your expression is allocated in a heap using a pointer that is passed to your function, and you free memory afterward such that you wonât fill up memory. For us, it is enough explanation but if you want to know more, I would recommend you to understand memory management.
// It encodes your input.
const expression = encoder.encode(evt.target.value + "\x00");
// It allocates enough space in the heap, and also it returns a pointer to heap.
const pointer = calculator.exports.allocateMemoryForString(expression.length + 1);
// It sets the encoding expression in the heap.
const memory = new Uint8Array(calculator.exports.memory.buffer);
memory.subarray(pointer).set(expression);
// It evaluates your expression where the pointer refers to it.
calculator.exports.calculate(pointer);
// It frees memory
calculator.exports.freeMemoryForString(pointer);
Our deal arises about building a fulfill calculator
At least 70 years before, people invented compilers in order to take symbolic input from a keyboard because these problems are tricky when you don't have the theory, but donât worry I promise you it is not a lot. Remember, if someone ignores good standard answers with no piece of evidence, she/he is a moron.
The answer to your problem is âfree context grammarâ; it solves significant challenges in formal languages. Your calculator project doesnât need a full theory, so you may ignore some details. Grammar describes a language in a compact way.
For example, a simple English sentence
You fly : Subject Verb
So, its grammar is
Sentence := Subject Verb
This representation way is called the Backus-Naur form.
Our arithmetic expressions with precedence rules are
2+2*2 : Number+Number*Number : Number + Rule1 : Rule0: Expression
1-2-3 : Number-Number-Number : Number-Rule0 : Rule0 : Expression
(2+2)/2= : (Expression)/Number : (Number+Number)/Number : Expression
their grammar is
Expression := OperatorPrecedenceLevel0
OperatorPrecedenceLevel0 :=
OperatorPrecedenceLevel1|
OperatorPrecedenceLevel0 "+" OperatorPrecedenceLevel1|
OperatorPrecedenceLevel0 "-" OperatorPrecedenceLevel1|
OperatorPrecedenceLevel1 :=
OperatorPrecedenceLevel1|
OperatorPrecedenceLevel1 "*" OperatorPrecedenceLevel2|
OperatorPrecedenceLevel1 "/" OperatorPrecedenceLevel2|
OperatorPrecedenceLevel1 "%" OperatorPrecedenceLevel2|
OperatorPrecedenceLevel2 :=
Number|
"(" Expression ")"
Number := Integer|Integer"."|Integer"."Integer
Integer := <Digit><Integer>|Digit
Digit := "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
Your grammar increases operator precedence (it is grouped before) at new levels.
If you want to test grammar, you would like https://web.stanford.edu/class/archive/cs/cs103/cs103.1156/tools/cfg/
Turning grammar into code. A program called parser.
You turn grammar into code using the simplest way: writing each grammar rule in a function that evaluates tokens, starting with the first rule, and parsing quote symbols as tokens (sometimes they are called terminal symbols).
A full-fledged evaluator shed light on you.
Testing
If you like unit testing, you would like the Google Test framework for C++ code.
Weâre going to make a test suite as follows:
https://github.com/sanchezcarlosjr/my-first-compiler-aka-calculator/blob/main/src/evaluator.cpp
Execute your tests with
make live_test
When you change calculator_test.cc or evaluator.cpp files âmakeâ compiles them and shows results.
Exercises
It's expected you don't import any mathematics library.
- Turn factorial to Gamma function.
- Implement trigonometric functions. Hint: Taylor theorem.
- Implement the Fibonacci function.
- Implement exponential function ï»ż, a**x. Hint: Newton-Rapshon method.
- Implement bitwise operations.
- Implement max and min functions.
- Implement a sort function.