I wrote these applications in C++11 with the wxWidgets GUI engine. This page is primarily intended for people who would like some high-level background on the applications themselves.
First, the theory. This is one implementation of a context-free language, namely, a type 2 (context-free) grammar in the Chomsky Hierarchy, with a recognizing machine in the form of a recursive descent LL(1) pushdown automaton (PDA).
For an explanation of all this, a couple of definitions are in order before getting into this application's implementation.
Linguistic context is the linguistic environment which surrounds the occurrence of a particular constituent. A constituent is a terminal or non-terminal symbol; more basically, anything from the alphabet of this particular language.
Linguistic context was used as far back as 1942 by the structural linguist Zellig Harris (Morpheme alternants in linguistic analysis. Language, 18:169-180. 1942). Harris was Chomsky's teacher and an enormous influence on generative grammar.
A 'context-free' grammar refers to anything on the left hand side of the production (i.e., everything to the left of the arrow, e.g., \(A \Longrightarrow \ldots\)) having no context, namely: there can be no letters whatsoever around the non-terminal \(A\) on the left-hand side of the arrow. If there are, then they are the context of that non-terminal, and we no longer have a context-free grammar.
There you have the theory, where everything is in the abstract. You can do it on paper, but there is no real-world, 1-to-1 equivalent (for example, no program) to the abstractions you play with on paper.
Here's an actual program that puts this into practice. Quickly we find that we have to attend to many other factors besides a cleaned-up abstraction that the theory provides.
The compiler is only partial because it has all of the analytical parts of a compiler: a lexical analyzer, a syntactic analyzer with semantic analysis, and both a symbol table and error handler (which both operate throughout the entire program, including translation, i.e., code generation). However, it does not do any translation into a lower-level language like assembly language or machine language. Instead, it gives the output of a standard calculator.
The partial compiler is where theory meets practice; where the rubber meets the messy road. As always in any science, practice is necessarily more disorderly than the clean abstraction of theory.
Here, then, is what the program actually consists of.
The lexical analyzer takes any input whatever, and distills from it only numbers/characters without spaces, as much as possible. If any of these strings is not an enduser-defined variable, the GUI will tell the user to input only valid formulas.
Strings are a concatenation of characters. We look at each character individually. If the character is a letter instead of a number, operator, or parenthesis, the program first looks at all letters starting from that first one, all the way up until the next number, operator, or parenthesis. Then it checks the string of letters against the symbol-table to see if it matches any of our user-defined variables.
We have implemented the abstract grammar into a concrete recursive descent parser. This includes semantic analysis: if our operator is addition, we define what the machine does (the semantics) very differently from e.g., multiplication.
The recursion is indirect: function A calls function B calls function C calls function A.
The program gets the first token: a number, an open parenthesis, or a previously user-defined variable. If none of these are present, the program will tell you. It then runs the lookahead function on this token, putting it in the buffer, so it can use this token as its LL(1) lookahead token. This is before the rest of the syntax analyzer functions are run, so that it will truly be a look ahead.
Next, the program dives into syntax analysis proper. Here there is a (recursive) chain of functions. These functions, i.e., C++'s internal stack, plays the role of the pushdown automaton. Because it is Last In, First Out (LIFO) order, we get operator precedence as a reverse of the functions called first. Namely, the last function to be called (recursively) is effectively the first function to be executed. This is how we determine order of operations: unless there are parentheses, the order is as follows: the last function to be called, gets the result back first.
A screenshot of this program follows (click to enlarge in a separate tab):
This app is to help you out with your studies of formal logic. Enter the formula, and it will return a truth-table with all the subformulas showing their values as well.
The truth-table application:
Here is a screenshot of the application (click to enlarge in a separate tab):
I used C++11 for coding both of these applications, because C++ gives as much (or nearly as much) control over the machine's memory as is possible for a high-level language.
C++ also has many faults, though they're still around mostly because of backwards-compatibility. For example, we wouldn't want the C++ code on the Mars rovers to become obsolete any time soon.
And there's always a trade-off: control for ease of programming.
For example, C++ has the option of either putting a class object on the stack or on the heap. The default is to place it on the stack. But if you put an object on the heap with the new keyword, you must delete it to avoid memory leaks. It can be very difficult to remember to do this at all times, especially for longer/more complex applications that give your human memory a good workout.
"Resource Acquisition Is Initialization" (RAII) is a technique which puts all new keywords in a class's constructor, all deletes in its destructor, and so is a solution to this problem of human memory, focus, and attention. But you still have to remember to use RAII in your designs, and it takes quite a few more lines of code to do it right. This is just one example of the near absolute control C++ provides, at the trade-off of having to be explicit in your memory allocation code.
Java on the other hand has no equivalent to this. Java only gives you the option of creating an object with the new operator: there are no class objects on the stack. Then Java deletes the objects with its garbage collector whenever it deems appropriate: there's nothing you can do to delete the objects yourself. Though this trade-off eliminates a large aspect of human error, it also removes very much control over how your program does what it does. It's all a balance of what you really need.
C++ also gives you the ability to explicitly pass, return, and move objects in the following ways:
In short, other languages simplify the developer's life by removing various degrees of control of the machine. So long as the language is (semantically) Turing complete, there are certain times when this is better. For example, for any web applications, Node or PHP are (much) more direct than C++. But C++ is still king of control, and because I created these two applications as desktop applications, I wanted full control of how I went about programming them.