Weekend interpreter

Java (11), Swing (Single document interface), WeekendTextEditor is used as an application template.

Creating your own programming language is a useful thing. Firstly, it is interesting. Secondly, it can be used, for example, for acceptance testing, that is, for writing acceptance tests (Robert C. Martin. Agile Software Development: Principles, Patterns, Practices. Chapter 4. Testing. "Acceptance tests are programs and are therefore executable. However, they are usually written in a special scripting language created for customers of the application."). Or, for example... Have you tried to write a program for calculating wages? Some accruals, deductions, and all this depends on a huge number of conditions. Of course, you can take all this into account, program it. Or you can give the accountant a simple language for describing the calculation rules. The main thing is not to tell the accountant the words "programming language", but to call it "setting up" accruals and deductions. :-)

So, I took my text editor and divided its central section into two: the top section contained the editor, and the bottom section contained a panel that would display everything my interpreter outputs. Then I added a "Run" menu with "Run" and "Stop" options. And then I implemented an interpreter for my fictional programming language, Weekend Game Language (default file extension: WGL), which executes the program opened or written in the editor.

Eclipse was used for development. The project is located here: https://github.com/weekend-game/weekendinterpreter/ (EN) and here: https://gitflic.ru/project/weekend-game/weekendinterpreter/ (RU).

How to run the program

Download the repository to your computer. Everything you need for the program is located in the app folder. Navigate to the app folder and run the program by double-clicking the WeekendInterpreter.jar file or, if the program doesn't start, double-click the WeekendInterpreter.bat file. If the program doesn't start, download and install Java 11 or later and repeat the steps above.

How to open a project in Eclipse

In Eclipse, select "Import..." from the "File" menu. In the window that opens, select "Existing projects into workspace." Navigate to the folder with the downloaded repository and click "Finish." The project will open in Eclipse. In the Package Explorer (on the left side of the screen), double-click the WeekendInterpreter.java file. The file will open for editing (in the center of the screen). Run the program by pressing Ctrl+F11 or using your preferred method for running programs in Eclipse.

How to use the program

At the top of the application window, write a program in a language I've named Weekend Game Language (the default file extension is WGL). Run it by pressing F5 or clicking the green square button in the toolbar, or by selecting "Run" from the "Run" menu. If the program gets stuck in a loop, stop it by pressing the Escape key or the red square button in the toolbar, or by selecting "Stop" from the "Run" menu.

Description of the programming language Weekend Game Language

Backus-Naur Form (BNF) is convenient for describing language syntax. However, the most popular textbooks on various programming languages ​​don't use it. Well, I guess I'll do without it, too.

Comment

REM Program Example

Variable

A variable in the language is denoted by a letter of the Latin alphabet. Thus, 26 variables can be used. Variables have an integer type.

Not very impressive? But you have to understand that this is just a taste of interpreter development, knowledge of how it's generally done, a template for future adaptation to specific needs, the Hello World of interpreter writing.

A string is any sequence of characters enclosed in double quotation marks.

Assignment, expression

a = 7
b = 8 * (a + 5)

Supported operations: unary minus, +, -, *, /, % (modulo remainder), ^ (exponentiation). Operator precedence is traditional, but can be changed using parentheses.

Output (to the output area)

PRINT "a = ", a

Output with line feed

PRINTLN
PRINTLN "b = ", b
PRINTLN "b - 37 = ", b – 37

That is, after PRINT or PRINTLN, strings, variables, or expressions are listed, separated by commas.

Input

INPUT "Specify the value of X: ", x

A value entry box will appear on the screen, asking you to enter a value. You can choose not to enter it, but it will be interpreted as entering 0. Of course, it would be better to enter the value directly in the interpreter's output area. However, this complicates the code somewhat, and in this case, it is important that the implementation be as simple and easy to read as possible.

Unconditional jump (forgive me, Edsger Wybe Dijkstra)

GOTO 10

The number 10 is a program line label. To mark a line and thus indicate where to jump, write it at the very beginning of the line. For example:

10 PRINTLN "Label 10"

By the way, the label in the GOTO statement doesn't necessarily have to be a constant. It can be a variable or any expression.

Conditional jump or conditional execution of a single command

IF a > 5 THEN GOTO 20

You don't have to write GOTO; you can write any statement, but only one.

The following comparison operators are supported: "<", ">" , "=", "#".

Loop

FOR i = 3 TO 7
   statements
NEXT

Subroutine

1000
PRINTLN "This is subroutine 1000"
RETURN

Subroutines don't have names; they are identified by labels. They should be placed after the main program. A subroutine ends with the command

RETURN

Call a subroutine

GOSUB 1000

End a program

END

You can leave the end of a program blank, but subroutines may follow the main program, in which case you should use END to separate them.

Example Program

You can see the application of all the above constructs by running the CommandsDemo.wgl program (included in the repository).

How the program is written

The interpreter is built on a previously created text editor as an extension of the text editor, allowing it to execute the file being edited, provided that the file contains a program. The interpreter is fairly self-contained (game.weekend.interpreter package). The editor provides it only with the name of the current file and a panel for displaying messages and text output by the PRINT language command.

The interpreter is launched using the Runner.run() method. This method opens the current editor file and creates an Interpreter class object in a separate thread, starting its execution by calling the execute() method.

The Interpreter constructor creates the necessary objects for operation, which exist for the entire duration of the interpreter's execution.

The Text class object is a wrapper for the interpreted file. It provides methods for conveniently reading program text, but it is too low-level for interpretation. The Text class object is used by the TokenReader token reader. TokenReader, using Text, reads the program and returns the next token. A token is an object of the Token class, which can be (type field): a delimiter (DELIMITER), a string (STRING), a number (NUMBER), a variable (VARIABLE), or a command (COMMAND). Since this object doesn't make any changes to its data, I decided to do without get methods. Class variables are declared final and cannot be modified. The execute() method reads the tokens sequentially. If the token is a command, the method responsible for implementing the command is called. All of these methods are located in the Command class. If the token is a variable, I consider this to be an assignment operator.

Variable

As mentioned in the language description, it supports only 26 variables, each of which is a letter of the Latin alphabet. The Variables class is responsible for working with variables. This class contains a 26-element array that stores variable values, and two methods for retrieving and assigning a value to a specified variable. This is a simple class.

Assignment

So, if the token is a variable, we read the next token and expect it to be a delimiter (DELIMITER), specifically the = symbol. If it isn't, there's an error. If so, we read the following expression using the Expressions.getExp() method. Generally, whenever we expect an expression to follow, we call the Expressions.getExp() method.

This method is a bit tricky to understand if you're not familiar with the term "recursive descent method." But, simply put, we read the next token, store it in the token class variable, read the number using the level2() method, return the token to the token reader for further reading, and return the read number to the calling program.

Unclear? Complete nonsense? Well, I warned you.

level2() is responsible for the + and - operations. These are the lowest-priority operations. In this method, we read the number using level7(). Basically, we read using level3(), but we'll skip levels 3, 4, 5, and 6 for clarity. Level7() returns the value from the token stored in the token class variable. This must be a number or a variable. It will then immediately replace the contents of token with the new token.

Next, we look in the token class variable: is it + or -? If so, we read the new token and store it in place of the previously read one. We read the second number using level7() and perform + or - on the resulting numbers, returning the result. If it wasn't + or -, we simply return the value obtained at the lower level.

level3(), 4, 5, and 6 are methods similar to level2(), but they are designed for the operations *, /, %, ^, unary + and -, and parentheses. This is the implementation of operator precedence. The highest level is a number or variable, followed by parentheses, unary + or -, ^, *, /, %, and finally + and -.

Now I'll describe the command implementation.

REM Command

I command TokenReader to move a line in the file being read by calling TokenReader.nextLine(). In other words, I skip everything written to the end of the line.

PRINT Command

I read tokens in a loop. If it's a line feed or end of file, the loop ends. If it's a string, I output it; otherwise, I try to read and evaluate the expression. I output the result. If the next token is ';' or ',', I continue the loop; otherwise, I terminate.

PRINTLN Command

This is the same PRINT command, but I output a line feed at the end.

INPUT Command

I read the next token. If it's a string, I remember it and read the next token. Here, I expect the variable to be read, which will store the user's input. Actually, I use JOptionPane.showInputDialog() for input. I understand this isn't very good, and it would be better to have the input in the output panel, but this interpreter is just an exercise in writing interpreters, and I decided to keep the program simple.

GOTO Command

I read an expression. I assume that GOTO is followed by a line label (a number) where control should be transferred. Using the Labels.goToLabel(label) method, I transfer control to the specified label. Here, we need to focus on the implementation of labels.

Labels

An object of the Labels class is responsible for working with labels. When a Labels object is created, it scans the program text and reads the number at the beginning of each line. If one is found, the label number and the line number where it was encountered are placed in an ArrayList. Clearly, with such a list, it's easy to get the line number in the program text corresponding to the label number using the label number. And then, using TokenReader.setLine(line_number), set the current position for further reading of tokens.

IF Command

I read an expression - the left expression. Then I read the token and hope to get "<", ">" , "=" or "#". Then I read the right expression. I perform the appropriate comparison of the left and right expressions, and if the result is TRUE, I read the next token. It should be THEN. This should be THEN. If the result is FALSE, I move to the next line to read tokens, meaning I continue interpretation from the line following the IF. Therefore, the command following THEN will not be executed.

FOR Command

I read the token and check if it's a variable. This will be the loop variable. Then the '=' token must follow. Then the expression - this is the initial value of the loop variable. Then the 'TO' token is required. Then I read the expression again - this is the final value of the loop variable. If any of this goes wrong, it's considered an error. I create a ForItem object, which consists of three fields: the name of the loop variable, its final value, and the line number of the beginning of the loop body (the line number immediately following the FOR command). I push it onto the stack. That's all. The stack is needed here to handle nested loops. I expect some commands to follow the FOR command - the loop body, which will begin to be interpreted now - and then a NEXT command will definitely follow, ending the loop.

NEXT Command

I pop the ForItem object from the stack. I increment the loop variable by one (yes, in this implementation, the loop variable always increments, and only by one). If its value is still less than or equal to the final value, I push the ForItem object back onto the stack and set the current line to the beginning of the loop body. If not, I do nothing. This way, the tokens following NEXT will be processed. In other words, I terminate the loop body repetition.

The GOSUB Command

After the GOSUB token, I read an expression. Yes, in this implementation, subroutines are identified by numbers, not names. Then I remember the line number following the GOSUB command and push it onto the subroutine stack (a regular stack). I move to the line marked with the specified label (the subroutine number). I expect the subroutine to terminate with the RETURN command.

RETURN Command

I read the line number following the GOSUB command from the stack, previously pushed there, and set it as the current line for further reading by TokenReader. In other words, I terminate the subroutine and transfer control for further program execution.

END Command

There is no method corresponding to this token in the Commands class. If this token is encountered, the interpreter simply terminates.

You can learn more about the program's operation by downloading the project, opening it in Eclipse, reading the class code, making changes, running the program, and watching the results.

Results

An interpreter has been created for a language similar to either Fortran or the very old Basic used by our fathers, grandmothers, and some of us. It's interesting as a test version of an interpreter. It could be useful as a template for adapting it to specific needs.

It would be nice...

It would be nice to add line numbers in the editor and highlight keywords in color.