Use Processing to implement an execution environment for your own live coding language with an editor console preview screen

Introduction

This is the one announced in the EP exercise of the FMS department of Meiji University on September 17, 2020. Well, I don't care about that, so please take a look. I put the source code on github.

Overview

The program itself is implemented using a language called Processing, which seems to be a cover of java. The execution screen consists of a text editor, console, and preview screen. D4A19692-CDAF-4B48-B178-A0C1720EA227.jpeg

Language grammar

Basically, it can be written in the same syntax as java. There are some parts that are different from java, so I will explain a little.

Variable declaration

The variable declaration has the following syntax.

let a = 10;
println(a); // -> 10
println(typeof(a)); // -> int

let b: float = 10;
println(typeof(b)); // -> float

Currently available built-in types

4 types and their arrangement. If you feel like it, I want to increase it soon. The array declaration can be written as:

let arr1 = {0, 1, 2};
let arr2 = {{0, 1}, {1, 2}};
let arr3 = int[2][2]; // make {{0, 0}, {0, 0}}

Also, there is Rust in a language with a similar grammar, but in the case of Rust, if you declare it with let, it becomes a constant, and if you add mut, it becomes a variable, whereas in this language, let The variable is declared with. I haven't made a constant declaration yet, but I'm thinking of making it a const statement like JavaScript.

You can also write the following.

let c, d = 10, 20;
println(c, d); // -> 10 20
c, d = d, c; // swap
println(c, d); // -> 20 10

Function definition

The function can be defined with the following syntax.

fn add(a: int, b: int) -> int {
    return a + b;
}

Supplement

Since it is made by a lot of rush work, there are many built-in functions that can not be supported compared to other languages, and there are a lot of problems such as not being able to support object orientation in the first place. please forgive me...

how to use

Next, let's see how this program works.

Window generation

Creates a window (corresponding screen) in the preview screen. This can be done in a similar way to Processing. 369C08DD-1B9F-4309-B204-637C42AAEEE9.jpeg

In this example, the screen with the ratio corresponding to the 800x600 window is displayed on the preview screen. You can also specify the background color using the background () function, just like Processing.

Using the console

To print the string to the console, use the println () function. If you don't want to print a newline at the end of the line, you can also use the print () function. F741BD52-53ED-4A46-BC77-8EB8A0C5A810.jpeg

The ability to enter into the console has not yet been implemented. sorry.

Processing for each frame

Similar to Processing, you can write frame-by-frame processing by defining the draw () function. The following example implements the behavior of gradually changing the background color from black to white. B1C3DE40-D435-4DBB-B7AC-779F4C83A61B.jpeg C21F3A6A-0BD1-4D9A-93F4-3C7D743AD2EE.jpeg 6CA58A85-3ECC-4D18-B0F5-1C33075FC954.jpeg

Processing for mouse

Implemented the function to call when the mouse is clicked if the mouseX, mouseY and the mousePressed () function that indicate the mouse coordinates are defined. So, you can write the following processing that inverts the clicked part and the squares on the top, bottom, left, and right. 083D3856-E7E3-4756-A4E1-2C5EFAABF1A8.jpeg C1B173B7-AF1C-4601-AA90-37E710CF0522.jpeg 3AE8D23F-CBA3-49ED-86BF-BA14C93DE979.jpeg

The code is cut off, so I'll put it separately.

size(500, 500);

let board = int[5][5];
let count = 0;

board[0][2] = 1;
board[1][1] = 1;
board[1][2] = 1;
board[2][3] = 1;
board[2][4] = 1;
board[3][3] = 1;

fn draw() -> void {
    background(0);
    fill(255, 255, 0);
    count = 0;
    for(let i = 0; i < 5; i++) {
        for(let j = 0; j < 5; j++) {
            if(board[i][j] == 1) {
                rect(100 * i, 100 * j, 100, 100);
            }
            else {
                count++;
            }
        }
    }
    if(count == 25) {
        textSize(32);
        textAlign(CENTER);
        text("CLEAR!", width / 2, height / 2);
    }
}

fn mousePressed() -> void {
    let x = mouseX / 100;
    let y  = mouseY / 100;
    for(let i = max(0, x - 1); i <= min(4, x + 1); i++) {
        board[i][y] = 1 - board[i][y];
    }
    for(let i = max(0, y - 1); i <= min(4, y + 1); i++) {
        board[x][i] = 1 - board[x][i];
    }
    board[x][y] = 1 - board[x][y];
}

Built-in functions that can be used

In the above program, some functions that you are familiar with in Processing have appeared. Some of the functions available in Processing are implemented for use in this homebrew language as well. Currently available built-in functions are all of the following, with or without Processing.

Internal implementation

Now let's move on to the detailed implementation inside. From this point on, it may be a little difficult for those who have insufficient knowledge of Processing or who do not have much programming experience.

Editor implementation

Character input

It is implemented by making full use of keyPressed, keyPressed () function, keyTyped () function, etc. First, let's take a look at the following code inside the draw () function.

if(keyPressed) {
    if(prevKey.equals(String.valueOf(key))) {
        if(keyt < dur) keyt++;
        else {
            keyt = dur / 2;
            if(key != CODED) keyTyped();
        }
    }
    else {
        keyt = 0;
        if(key != CODED) keyTyped();
    }
}
else {
    keyt = 0;
    prevKey = "";
}

prevKey is a variable that holds the previously entered key. keyt represents how many frames have passed since the key was entered. dur is the value set as the threshold for keyt. If keyt exceeds the threshold dur while the same key as last time is entered, substitute dur / 2 for keyt and key shift You are calling the keyTyped () function when it is not an encoded key such as or Control. If a key different from the previous one is entered, keyt is not checked and 0 is assigned to keyt. Also, the keyPressed () function has the following implementation.

void keyPressed() {
    if(key == CODED) {
        if(keyCode == LEFT) {
            cursor.prev();
            beforeCursor.prev();
        }
        else if(keyCode == RIGHT) {
            cursor.next();
            beforeCursor.next();
        }
        else if(keyCode == UP) {
            cursor.up();
            beforeCursor.up();
        }
        else if(keyCode == DOWN) {
            cursor.down();
            beforeCursor.down();
        }
        else {
            keyFlags.put(keyCode, true);
        }
    }
}

cursor, beforeCursor is a Cursor type object and has methods line, row to represent rows and columns. cursor represents the current cursor position. Since beforeCursor is used to implement range selection, I will explain it later. Also, keyFlags is a HashMap <Integer, Boolean> type object, and indicates whether keyCode indicating an encoded key other than the cross key is entered. In this keyPressed () function, the cursor is moved when the cross key is pressed, and keyFlags is changed when other encoded keys are input. .. Also, in order to change keyFlags when the key whose keyFlags is true is released, I defined the keyReleased () function as follows.

void keyReleased() {
    if(key == CODED) {
        if(keyFlags.get(keyCode) != null) {
            keyFlags.put(keyCode, false);
        }
    }
    keyPressed = false;
}

The process of changing keyPressed to false should not be necessary, but if you do not write it for some reason, the behavior of keyPressed will be strange, so I will write it.

The keyTyped () function describes the behavior corresponding to each key entered. If you write it here, it will be long, so if you want to see it, you may want to check it from Source code link.

Display of character string

The entered string is stored in a ʻArrayList <String> `type object line by line. It just displays them in order. When the number of lines exceeds the display range of the editor, the scroll bar is displayed on the right side of the editor. The height at which the character string is output changes according to the position of the scroll bar.

Feature implementation

In order to call yourself an editor, if you can just enter characters, it is no different from Notepad, so you need some functions. The following are the functions of the editor currently implemented.

The implementation of range selection is achieved by having one of the beforeCursor and cursor as the beginning of the range and the other as the end of the range. The rest is messed up in the keyTyped () function, so if you're interested, take a look.

Console implementation

The console also stores strings in the ʻArrayList <String> `type object. The display is the same as the editor.

Implementation of language processing system

The language implemented this time is a simple dynamically typed interprinter language. However, I don't like dynamically typed languages, so the code looks like a statically typed language doing type inference. The flow of processing is a little special, and the original lexical analysis and parsing are performed, and the abstract syntax tree is interpreted and executed, but the one implemented this time is a crude implementation that omits various things. It is. For example, consider running the following code.

let a = 10;

At this time, the Lexer class will generate the following calculation result.

[
    Token("let", "let"),
    Token("id", "a"),
    Token("expr", "expr"),
    Token("int", "10"),
    Token("endExpr", "endExpr"),
    Token("endLet", "endLet")
]

This is an object of type ʻArrayList <Token> `. An object of type Token has two fields, kind and value. Token (a, b) is written as Token whose value is kind and value is b. Think of it as representing a type object. Then, this array is interpreted as an instruction sequence and executed. Execution of the let statement is done by the following code.

    .
    .
    .
else if(res.get(i).kind.equals("let")) {
    ArrayList<String> vars = new ArrayList<String>();
    ArrayList<String> values = new ArrayList<String>();
    i++;
    String type = "", exprType = "";
    if(res.get(i).kind.equals("type")) {
        type = res.get(i).value;
        i++;
    }
    while(!res.get(i).kind.equals("endLet")) {
        if(res.get(i).kind.equals("id")) {
            vars.add(res.get(i).value);
            varNames.add(loc + "$" + res.get(i).value);
        }
        else if(res.get(i).kind.equals("expr")) {
            ArrayList<Token> expr = new ArrayList<Token>();
            i++;
            int exprNum = 0;
            while(exprNum > 0 || !res.get(i).kind.equals("endExpr")) {
                if(res.get(i).kind.equals("expr")) exprNum++;
                if(res.get(i).kind.equals("endExpr")) exprNum--;
                expr.add(res.get(i));
                i++;
            }
            exprType = calc(expr, loc);
            values.add(expr.get(0).value);
        }
        i++;
    }
    int vs = values.size();
    for(int j = 0; j < vars.size(); j++) {
        if(j < vs) {
            if(type.isEmpty()) {
                variables.put(loc + "$" + vars.get(j), new Variable(exprType, vars.get(j), values.get(j)));
            }
            else {
                variables.put(loc + "$" + vars.get(j), new Variable(type, vars.get(j), values.get(j)));
            }
        }
        else {
            if(type.isEmpty()) {
                variables.put(loc + "$" + vars.get(j), new Variable(exprType, vars.get(j), values.get(vs - 1)));
            }
            else {
                variables.put(loc + "$" + vars.get(j), new Variable(type, vars.get(j), values.get(vs - 1)));
            }
        }
    }
}

res is a variable of type ʻArrayList <Token> `` that receives the calculation result of `` Lexer``. `` Variables`` is a variable of type `` HashMap <String, Variable> `` that stores variables. `` varNames`` is a variable of type ʻArrayList that stores the names of variables with scope in mind. Variable scope management uses $ . Specifically, the scoped name of the variable ain the global area is main $ a, and the variable defined in the function func () in the global area. The scoped name of ```a is main $ func $ a. Since ʻexpr`` to ʻendExprrepresent an expression, pass the Token column from `ʻexpr to` ʻendExprto the calc () function. By doing so, you can calculate the formula. In this way you can execute the let`` statement.

For other statements, the Token column is generated and executed in the same way, so please refer to the implementation of the statement () function for the detailed implementation of each.

The end

There are many other parts that can explain the implementation, but freshness is the life of the story, so I would like to end the explanation here because I want to post an article on the day when this work is announced. If you have any requests, questions, suggestions, suggestions, taunts, ridicule, etc., such as wanting to add a commentary, please comment on this article or reply directly to My Twitter account. Or send a DM.

We deeply apologize for the miscellaneous implementation, miscellaneous commentary, and miscellaneous endings. sorry.

If you would like to hear a more detailed and polite explanation of the implementation of the language processing system, please see this article.

Recommended Posts

Use Processing to implement an execution environment for your own live coding language with an editor console preview screen
[Ruby on Rails 5] Use WorkMail to set an email address for your own domain. [Work Mail]