Wolf Interpreter

Wolf Interpreter

11 devlogs
41h 35m
Created by Pookstir

This is going to be a program which can interpret text files written in a language I'm designing called Wolf.

Timeline

So my plan was very simple: Add in the screen from the very first devlog and enough functions to make it useable. So I put the screen into the scene using a second split container (The same kind of container that separates the input from the console). I then set out to expose the three important cell methods to Wolf: setcellbgcolor, setcellcharcolor, and setcellchar. While testing them, I also decided to add wait to pause the code for a set amount of time and iskeydown to check for keyboard input without using the console. I then realized that the biggest limiting factor at this point is likely the lack of access to data structures like arrays.
This meant that I finally had to confront an issue that I've been thinking about for a while. The problem is as follows: I was storing the data types of things as strings. Strings which might as well have been an enum. This was fine...if you see a data type as a list of mutually exclusive options where you must pick exactly one. The problem is that this isn't how Wolf handles data types. Wolf's data types make heavy usage of inheritance, a feature which these strings just can't really store nicely.
To solve this, I decided to make each data type be a node on a tree, where the type's parent would be the type it inherits from and type's children would be the types that inherit from it. So far this is mostly an internal change but soon it's results will be user facing. This change did mean changing pretty much every part of the interpreter though so that's why I haven't devlogged in a bit

Update attachment

I added function calling (not function declaration yet) and made two new native functions for the console: clear_console and prompt. I also made print work like the other two native functions rather than being a special statement. While adding prompt, I had to turn the function that evaluates function calls into a coroutine because prompt waits for the user to input something. This lead to me having to make the entire chain that calls it use awaits. I had a bug that took me a while to figure out the source of because apparently, despite not relying on the outcome, the functions outside of the evaluator that cause the evaluation of statements also had to use await for this one case to work. But now the functions are working so that's good at least.

Update attachment

The first thing I did since the last devlog was create indented blocks. I temporarily had them work anywhere due to a lack of statements that used them. Blocks are interesting because they each get their own local scope as well as being able to access variables from outer scopes. The way this is implemented in the code is by creating a new instance of the environment for each block and giving that instance a reference to the parent environment for when external variables are accessed. After implementing blocks, I quit letting them be put anywhere and created if statements. If statements, as well as all of the other flow control statements, use indented blocks to indicate the code which is inside of the statement. After if statements were working, I made while loops which use very similar code to the if statements but without needing to handle elif/else cases. They really only start to deviate once you get to the evaluator. Then I added for loops which deviated by a lot from the other two. Pretty much the only part they share is the ending colon, new line, and indented block.

Update attachment

I created a new class for statements. It works similarly to the expression class but it represents something that would take up a whole line. There are three types of statements so far: Expression statements, print statements, and declaration statements. Expression statements just evaluate an expression. Print statements evaluate an expression and print the result as a string. Declaration statements create variables. I also created an expression type for variable references. After creating the statement class and variable expressions, I changed the parser to return an array of Statements instead of an expression. I also created a new class called the Environment which stores the types and values of all declared variables. I then changed the typer and the evaluator to work with the array generated by the parser. After the statements and variable references were working, I added assignment expressions to the interpreter.

Update attachment

Previously, variable types were checked at run time by the evaluator. This would be fine for a dynamically typed language. The problem is that Wolf isn't a dynamically typed language so it wants type checking to happen before the program starts running. At the moment, this doesn't really make a difference but once the interpreter starts handling more complex code, it will become a noticeable difference. It also enables type errors being checked at edit time once I get to that.
To achieve the shift I wanted, I added a new part to the compiler which goes in between the parser and the evaluator. I call it the typer. It works similarly to the evaluator but instead of trying to run each node of the tree, it tries to find the nodes' types and add any necessary implicit type conversions between different number types. I also made the parser and evaluator (and typer) handle a new operator: as. As casts a value provided on the left into the type provided on the right. The ability to perform this conversion is the one place where type related stuff is checked by the evaluator.

Update attachment

I created a manual describing what the different types are, what the operators are, and the order of precedence of the operations. I used Godot's tab container node so it was mostly just a lot of typing.

Update attachment

So remember how I said, creating a file dialog was pretty quick and easy since I'm using [G]odot? Well, I decided to create a web build. When I tried to run this build, I quickly learned that Godot's FileDialog node doesn't work on the web. Eventually I want to try to find a way around this but I decided that it was probably easier to make an area where you can edit your code. Eventually, the goal is that you would be able to use it or open a file from your computer but for now, the web build is going to only use the inbuilt text editor. The interpreter does have a web page now though so that's good at least. https://cattacocattaco.github.io/Wolf-Interpreter/ (It's also linked as the demo)

Update attachment

I built the third part of the interpreter which is the Evaluator. Although it is currently only built for the subset of the language which the parser parses. The evaluator's job is to take the nicely structured tree that the parser made and actually run the code. Currently, that just means finding the value that should be returned. The way that this evaluator works is by starting at the top most expression and recursively finding the values of all expressions that it relies on until it gets to the bottom of the tree where all of the literals are. Once it has reached the literals, it uses their values to calculate the values of the expressions that use them.

On another note, I realized that my test code didn't use any parentheses. When I tried testing an expression that used parentheses, I realized that the parser had a tiny bug where when reaching an opening parenthesis, it wouldn't actually consume the token and would instead recursively read the same opening parenthesis until it produced a stack overflow error. Anyways, I am now doing what I should have been doing for a while: Using actual test cases and actually checking the results.

Update attachment

I built the second part of the interpreter: The parser. Well, I built the parser for some of the language's features. It currently parses the ternary operator, binary logic expressions (and, or, xor, nand, nor, and xnor), negations, comparisons, bitwise operators, addition/subtraction, multiplication/division/modulo, exponents, unary minus/bitwise not, and literals and grouping expressions. Those are arranged from most to least priority with slashes between operations of the same priority.
The parser converts from the linear stream of tokens produced by the lexer into a tree which tells the computer what order to do things in.
Also, realized that I didn't pay enough attention to what the lexer was outputting and that keywords weren't being recognized properly due to a slight mess up in what variable the identifier's string was being stored to which only affected the token's type. So I fixed that bug.

Update attachment

The first thing I did since the last devlog was adding a menu for selecting a file to read the code from. This was pretty quick and easy since I'm using godot so I basically just had to put the built in file dialog node into a scene and connect its file selected signal to a script.
Then, I started creating the actual interpreter. The interpreter has three main parts: A tokenizer (also called a lexer), a parser, and an evaluator. So far, I have just built the tokenizer.
The tokenizer segments the raw text of the file into grammatical units like identifiers, parentheses, literals, and operators. These grammatical units are called tokens, hence the name tokenizer. The tokens are useful because they tell the parser what chunks of characters are connected and what part of the grammar is being represented by those characters.

Update attachment

I made a grid which divides the screen into n rows and n columns. Each cell in the grid can be given a color and can display a 6px by 6px character in a second color.
I made a 16x8 sprite sheet of characters for the cells to display, a bit over half of which are normal text characters while the 67th-115th are for drawing. Characters 116-127 are currently empty. I put the character mappings in a spreadsheet. The end goal would be that the program can add its own sprite sheet for characters 128-255.
I coded a snake game in gdscript to test that the cells and the grid are working.

Update attachment