DEV Community

Cover image for Let's create a mini programming language!
Posandu
Posandu

Posted on • Originally published at tronic247.com

Let's create a mini programming language!

In this tutorial, we will create a mini programming language. That compiles

center,
left 100,
color blue,
top 10,
right 100,
Enter fullscreen mode Exit fullscreen mode

to

_center();
_draw();
_left(100);
_draw();
_setColor("blue");
_draw();
_top(10);
_draw();
_right(100);
_draw();
Enter fullscreen mode Exit fullscreen mode

Which we will use to draw interesting stuff on the HTML canvas.

So let's start!

Basic stuff

Before doing the code, we need to know the logic.

Lexing

First, we lex the code. This is the process of splitting the code into tokens and removing unnecessary characters.

Validating

Then, we validate the code by checking if the tokens are valid.

Compiling

Then, we compile the code to JavaScript.

Programming part

We will name our mini-language drawlang. Its syntax looks like this.

  • center, left, top, right, bottom, color are keywords.
  • , is also a keyword that draws.
  • Any color like red, blue, green, black, white, ...etc is a color.
  • Any number is a number.

Setting the basic stuff

First, I'll create an index.js file and add this code.

const fs = require("fs"); // File system

const code = `
center,
left 100,
color blue,
top 10,
right 100,
`; // Sample code to test during the tutorial
Enter fullscreen mode Exit fullscreen mode

We import the fs module to read the file.

Creating the lexer

I'll create a function named tokenize and accepts a string as an argument.

function tokenize(code) // ...
Enter fullscreen mode Exit fullscreen mode

Now keep to variables to store the tokens and keep track of the current position.

const tokens = [];
let i = 0;
Enter fullscreen mode Exit fullscreen mode

And a utility function to add a token to the tokens array.

function addToken(type, value) {
    tokens.push({
        type,
        value,
    });
}
Enter fullscreen mode Exit fullscreen mode

Now, we can start tokenizing the code. We will use a while loop to iterate over the code.

while (i < code.length) // ...
Enter fullscreen mode Exit fullscreen mode

then we get the current character.

const char = code[i];
Enter fullscreen mode Exit fullscreen mode

We will use a switch statement to determine the type of the token.

switch (
    char
    // ...
) {
}
Enter fullscreen mode Exit fullscreen mode

We ignore all the whitespace characters.

case " ":
case "\t":
case "\n":
case "\r":
    i++;
    break;
Enter fullscreen mode Exit fullscreen mode

If it's a comma, we add a COMMA token.

case ",":
    addToken("COMMA", char);
    i++;
    break;
Enter fullscreen mode Exit fullscreen mode

Else, we check if it's a number or a keyword.

default:
    const isDigit = /\d/.test(char); // Returns true if it's a digit
    const isLetter = /[a-z]/i.test(char); // Returns true if it's a letter

    if (isDigit) {
        let number = ""; // Stores the number
        while (i < code.length && /\d/.test(code[i])) { // While the current character is a digit
            number += code[i];
            i++;
        }
        addToken("NUMBER", number); // Finally, we add the token
    } else if (isLetter) {
        let name = ""; // Stores the name
        while (i < code.length && /[a-z]/i.test(code[i])) { // While the current character is a letter
            name += code[i];
            i++;
        }
        addToken("NAME", name); // Finally, we add the token
    } else {
        throw new Error(`Unknown character: ${char}`); // 🤬 Error
    }
    break;
Enter fullscreen mode Exit fullscreen mode

And we finally escape the while loop and return the tokens.

return tokens;
Enter fullscreen mode Exit fullscreen mode

Now try adding this code and running node index.js and see the output.

const tokens = tokenize(code);
console.log(tokens);
Enter fullscreen mode Exit fullscreen mode

Nice! We have created the lexer/tokenizer.

Validating and compiling to JavaScript

We will create a function named compile that accepts an array of tokens as an argument.

function compile(tokens) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Then add some variables to store the compiled code and keep track of the current position.

let i = 0;
let out = "";

let addCode = (code) => {
    out += code + "\n";
};
Enter fullscreen mode Exit fullscreen mode

Again, we use a while loop to iterate over the tokens.

while (i < tokens.length) {
    // ...
    i++;
}
Enter fullscreen mode Exit fullscreen mode

This time we create a function to get the current token.

const token = () => tokens[i];
Enter fullscreen mode Exit fullscreen mode

And a function named expect to check if the next token is the expected one and if not, throw an error.

function expect(type) {
    if (tokens[++i].type !== type) {
        throw new Error(`Expected ${type}, got ${tokens[i].type}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

After that, we use a switch statement to determine the type of the token.

switch (token().type) // ...
Enter fullscreen mode Exit fullscreen mode

If it's a COMMA, we add _draw() to the compiled code.

case "COMMA":
    addCode("_draw()");
    break;
Enter fullscreen mode Exit fullscreen mode

Else, we check if it's a NAME token.

case "NAME":
/**
 * If the name is center, execute the center function
 */
if (token().value === "center") {
    addCode(`_center();`);
} else if (token().value === "color") {
    /**
     * If the name is color, expect a name and set the color
     */
    expect("NAME");
    addCode(`_setColor("${token().value}");`);
} else if (
    /**
     * If the name is left/right/top/bottom, expect a number and execute the corresponding function
     */
    token().value === "left" ||
    token().value === "right" ||
    token().value === "top" ||
    token().value === "bottom"
) {
    expect("NUMBER");
    const value = parseInt(token().value);

    addCode(`_${tokens[i - 1].value}(${value});`); // We get the token before the current one and use it to determine the function to call
} else {
    throw new Error(`Unknown name: ${token().value}`); // 🤬 Error
}
break;
Enter fullscreen mode Exit fullscreen mode

At last, we finally escape the while loop and return the compiled code.

return out;
Enter fullscreen mode Exit fullscreen mode

Now try adding this code and running node index.js and see the output.

const tokens = tokenize(code);
const compiled = compile(tokens);
console.log(compiled);
Enter fullscreen mode Exit fullscreen mode

If you did it right, you should see the following output:

We'll write the code in a file named out.js so you can run it in the browser.

const tokens = tokenize(code);
const compiled = compile(tokens);

console.log("Compiled code successfully!");
fs.writeFileSync("out.js", compiled);
Enter fullscreen mode Exit fullscreen mode

Nice! We now create a framework.js file that will contain the code for the framework.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

canvas.width = 900;
canvas.height = 900;

let x, y, lastX, lastY, color;
x = y = lastX = lastY = 0;
color = "transparent";

function setX(newX) {
    lastX = x;
    x = newX;
}

function setY(newY) {
    lastY = y;
    y = newY;
}

function _center() {
    setX(canvas.width / 2);
    setY(canvas.height / 2);
}

ctx.beginPath();

function _draw() {
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.strokeStyle = color;
    ctx.stroke();
}

function _left(distance) {
    setX(x - distance);
}

function _right(distance) {
    setX(x + distance);
}

function _top(distance) {
    setY(y - distance);
}

function _bottom(distance) {
    setY(y + distance);
}

function _setColor(newColor) {
    color = newColor;
}
Enter fullscreen mode Exit fullscreen mode

In the HTML file, we'll add the canvas and the script tag.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>

    <body>
        <canvas id="canvas"></canvas>

        <script src="framework.js"></script>
        <script src="out.js"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now run node index.js and open the index.html file in your browser.

This code shows this

center,
left 100,
color blue,
top 10,
bottom 100,
right 100,
top 200,
right 100,
Enter fullscreen mode Exit fullscreen mode

The end

If you need the code - here
Follow me on Twitter
Follow me on Github

Top comments (0)