[Note: I made this during my first sem at uni, so expect some not-so-well-written code ;)] Locks is an imperative, dynamically typed, procedure oriented scripting language based on the lox language. Locks-py is the python implementation of locks. While locks and lox share almost the same grammar, the locks implementation is not based on that of lox. This was made as a project for an introductory programming course.
Note: You will need to have python 3 installed. This project was tested with python 3.8.3.
To start the editor, clone this repo and run
python locks-editor.py
An editor window should pop up. Start coding! You can find examples at examples/
. To run a program, open a locks file through File -> Open
, and choose Run -> Run(debug)
from the menu bar. For more details, check the Editor section.
To run a locks program from the command line, run
python locks-interpreter.py <path-to-locks-file>
To be able to visualize the AST, the requests, cairosvg, and Pillow libraries will have to be installed.
pip install requests
pip install cairosvg
pip install Pillow
If these are not installed, the visualizer will generate a dot file, the contents of which can be pasted at GraphViz Online to render it. For more information, see Visualizing The AST.
The interpreter will use the VM to run the program by default.
locks-interpreter.py
accepts the following options:
Options | Description |
---|---|
path (required) | path to locks file |
-d (optional) | use tree walk interpreter instead of VM. |
-b output-filename (optional) | output code generated by compiler to specified file |
-v (optional) | output code generated by compiler to stdout |
-g output-filename (optional) | generate dot, svg, and png file to visualize AST generated by parser |
-h | show usage |
The print()
function prints arguments to stdout. println
adds a trailing newline character.
print("Hello World");
println("Hello World");
The input()
function accepts input from stdin. It accepts a prompt as string, and returns the inputted string.
input(""); // no prompt
input("Enter something"); // input with prompt
var x = input("Enter something"); // store input in a variable (as a string)
Locks supports single line and multi-line C-style comments. Multi-line comments cannot be nested.
// single line comment
/*
Multi-line comment
*/
The following are reserved keywords in Locks:
var, fun, if, elsif, else, while, for, return, continue, break, and, or, true, false, nil
A valid Locks identifier may contain alphanumeric and '_' characters, and may not begin with a number.
var _a123_; //valid
var 12r; //invalid!
Locks supports the following datatypes:
Nil
: Used to define a null value, denoted by thenil
keywordNumber
: Can be 64 bit signed integer, or double precision floating-point numbers. For example:135, 31.63, -1331
String
: Sequence of ascii characters surrounded by"
. For example:"Hello!"
Boolean
: Can betrue
orfalse
Array
: A sequence of Locks datatypes, surrounded by[
and]
and separated by,
. For example:[1, "hello", [true, 2]]
The following are falsey values in locks: 0
, ""
, []
, false
, nil
, and functions.
Variables are declared using the var
keyword.
var a; // declare
a = 5; // assign
var b = 5; // declare and assign
Locks supports the following statements (apart from loops, return, continue and break statements):
Locks supports the following operators for arithmetic and logical expressions:
-
+
: Adds two numbers or concatenates two strings. Both operands must be of the same type (i.e. String or Number) -
-
: Subtracts right operand from left operand. Both operands must be numbers. Can also function as a unary negation operator. -
*
: Multiplies two numbers -
\
: Divides two numbers -
%
: Gives remainder when left operand is divided by right operand (modulo) -
==
: Returnstrue
if left operand is equal to right operand, otherwisefalse
-
!=
: Returnstrue
if left operand is not equal to right operand, otherwisefalse
-
<
: Returnstrue
if left operand is less than right operand, otherwisefalse
. Both operands must be numbers -
>
: Returnstrue
if left operand is equal to right operand, otherwisefalse
. Both operands must be numbers -
<=
: Returnstrue
if left operand is equal to right operand, otherwisefalse
. Both operands must be numbers -
>=
: Returnstrue
if left operand is equal to right operand, otherwisefalse
. Both operands must be numbers -
and
: Returnstrue
if both left and right operands are truthy, otherwisefalse
-
or
: Returnstrue
if either the left or the right operand is truthy, otherwisefalse
-
!
: Performs a logical not on its operand
Unlike Lox, variable assignment is a statement in Locks rather than an expression. The left operand for the =
(assign) operator can be either an identifier, or an indexed identifier that refers to an array.
var a;
// assignment statements
a = [1,2,3,4,5];
a[3] = 7;
The if-elsif-else statement is similar to that of C.
var a = 3;
var b;
if(a < 10){
b = 0;
}elsif(a >= 10 and a < 20){
b = a;
}else{
b = 1;
}
A conventional C-style while loop, in which the body is executed while the expression specified in parentheses evaluates to true.
var i = 0;
while(i < 10){
println(i);
i = i + 1;
}
Locks supports C-style for loops. However, it must be noted that new scopes are created only when a function is called, so a variable if declared once in a for loop, it must not be declared again.
// infinite loop!
for(;;) println("Hello");
// Ok. Note that locks does not have a '+=' or '++' shorthand
for(var i = 0; i < 10; i = i + 1){
println("Hello");
}
// Ok.
for(i = 0; i < 10; i = i + 1){
println("Hello");
}
// ERROR! Duplicate declaration of variable 'i'
for(var i = 0; i < 10; i = i + 1){
println("Hello");
}
Functions in Locks are declared using the fun
keyword. The parameters must be comma separated identifiers, and are specified in parentheses after the function name.
fun add(b, c, d){
println(b + c + d);
}
add(1, 3, 5);
A value can be returned at any point from a function using the return
keyword.
// "fun fact"! :D
fun fact(n){
if(n == 1) return 1;
return n*fact(n-1);
}
println(fact(6));
Note that while functions can be declared inside functions, they cannot be returned from a function or assigned to a variable.
fun a(){
// Ok.
fun b(){
return 5;
}
println(b());
return b; // ERROR! can't return a function from within a function
}
var x = a; // ERROR! can't assign a function to a variable
print
: Accepts 1 argument and prints it to stdoutprintln
: Accepts 1 argument and prints it to stdout, with newlineinput
: Accepts 1 argument and prints it to stdout, and accepts input from stdin
For usage of these functions, check IO.
isinteger
: Accepts a string as argument, returns true if the string is a valid integer, false otherwise
len
: Accepts a string or an array as argument, and returns its length
int
: Accepts 1 argument, converts it to an integer, and returns itstr
: Accepts 1 argument, converts it to a string, and returns it
The Locks VM is inspired by the JVM and the Python VM.
Byte code for the Locks VM always begins with the magic number 0x04D69686F
, followed by the constants pool count (2 bytes) followed by constants. This is then followed by the function count (2 bytes) and then the functions. Each function begins with an argument count (2 bytes) followed by the length of the function code (2 bytes).
For example:
0x4d 0x69 0x68 0x6f // magic number
0x00 0x03 // constants pool count
0x08 0x48 0x65 0x6c 0x6c 0x6f 0x21 0x00 // constant 1 - string
0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0xf4 // constant 2 - int
0x06 0x00 0x20 0x00 0x00 0x00 0x00 0x01 0x3a // constant 3 - float
0x00 0x02 // function count
// function 1
0x00 0x00 // arg count
0x00 0x12 // code length (in bytes)
0x64 0x0 0x0
0x64 0x0 0x1
0x64 0x0 0x2
0x10 0x03
0x10 0x04
0x83 0x01
0x84 0x01
0xff
// function 2
0x00 0x02 // arg count
0x00 0x0a // code length (in bytes)
0x5a 0x00
0x5a 0x01
0x52 0x00
0x52 0x01
0x17
0x53
This is the code generated for:
// constants to demonstrate constants pool
"Hello!";
500;
3.14;
fun add(a, b){
return a + b;
}
println(add(3,4));
The constants pool stores all strings, negative integers and integers greater than 255, and floating-point numbers. Each constant is tagged to denote their type:
Datatype | Representing Tag |
---|---|
Integer | 0x03 |
Double | 0x06 |
String | 0x08 |
Negative integers are stored in their two's complement representation. For example, -1729
will be stored as 0xff 0xff 0xff 0xff 0xff 0xff 0xf9 0x3f
.
Strings are stored as null-terminated strings. For example, "Hello"
will be stored as 0x48 0x65 0x6c 0x6c 0x6f 0x21 0x00
.
Floting point numbers are stored according to the following representation:
For example, 3.14
will be stored as 0x00 0x20 0x00 0x00 0x00 0x00 0x01 0x3a
.
Opcode | Name | Description |
---|---|---|
0xFF | END | Ends execution |
0x01 | LOAD_NIL | Pushes a nil object on the operand stack of the current frame |
0x02 | LOAD_TRUE | Pushes a Boolean object with value "true" on the operand stack of the current frame |
0x03 | LOAD_FALSE | Pushes a Boolean object with value "false" on the operand stack of the current frame |
0x64 | LOAD_CONST | Pushes a constant from the constants pool on the operand stack of the current frame at index specified by its 2 byte argument |
0x17 | BNARY_ADD | Pops 2 items from the operand stack of the current frame, adds them, and pushes the result back onto the stack |
0x18 | BINARY_SUBTRACT | Pops 2 items from the operand stack of the current frame, subtracts them, and pushes the result back onto the stack |
0x14 | BINARY_MULTIPLY | Pops 2 items from the operand stack of the current frame, multiples them, and pushes the result back onto the stack |
0x15 | BINARY_DIVIDE | Pops 2 items from the operand stack of the current frame, divides them, and pushes the result back onto the stack |
0x16 | BINARY_MODULO | Pops 2 items from the operand stack of the current frame, performs the modulo operation, and pushes the result back onto the stack |
0x40 | BINARY_AND | Pops 2 items from the operand stack of the current frame, performs a logical and on the two items, and pushes the result back onto the stack. Note that unlike the Python VM, BINARY_AND performs the logical and, and not bitwise and. |
0x42 | BINARY_OR | Pops 2 items from the operand stack of the current frame, performs a logical or on the two items, and pushes the result back onto the stack. Note that unlike the Python VM, BINARY_OR performs the logical or, and not bitwise or. |
0x0C | UNARY_NOT | Pops 1 item from the operand stack of the current frame, pushes false if the popped value is truthy, and true otherwise. |
0x0B | UNARY_NEGATIVE | Pops 1 item from the operand stack of the current frame, negates it, and pushes the result back on to the stack |
0x5A | STORE_LOCAL | Pops 1 item from the operand stack of the current frame, stores it in a local variable at index specified by 1 byte argument |
0x61 | STORE_GLOBAL | Pops 1 item from the operand stack of the current frame, stores it in a global variable at index specified by 1 byte argument |
0x10 | BIPUSH | Pushes 1 byte argument as 1 byte integer on the operand stack of the current frame |
0x52 | LOAD_LOCAL | Pushes value of local variable at index specified by 1 byte argument on the operand stack of the current frame |
0x74 | LOAD_GLOBAL | Pushes value of global variable at index specified by 1 byte argument on the operand stack of the current frame |
0x67 | BUILD_LIST | Pops argument (2 bytes) number of items from the operand stack of the current frame, builds an Array object containing the items, and pushes it on the stack |
0x19 | BINARY_SUBSCR | Pops index from the operand stack of the current frame, pops array object from the stack, pushes the value at index of the array object |
0x3C | STORE_SUBSCR | Pops index from the operand stack of the current frame, pops array object from the stack, pops value from the stack, stores value at index of array object |
0x9F | CMPEQ | Pops 2 items from the operand stack of the current frame, pushes true of they are equal, false otherwise |
0xA0 | CMPNE | Pops 2 items from the operand stack of the current frame, pushes true of they are not equal, false otherwise |
0xA3 | CMPGT | Pops 2 items from the operand stack of the current frame, pushes true if 2nd item is greater than 1st item, false otherwise |
0xA1 | CMPLT | Pops 2 items from the operand stack of the current frame, pushes true if 2nd item is less than 1st item, false otherwise |
0xA2 | CMPGE | Pops 2 items from the operand stack of the current frame, pushes true if 2nd item is greater than or equal to 1st item, false otherwise |
0xA4 | CMPLE | Pops 2 items from the operand stack of the current frame, pushes true if 2nd item is less than or equal to 1st item, false otherwise |
0x70 | POP_JMP_IF_TRUE | Pops 1 item from the operand stack of the current frame, jumps to location specified by 2 byte argument if the item is truthy |
0x6F | POP_JMP_IF_FALSE | Pops 1 item from the operand stack of the current frame, jumps to location specified by 2 byte argument if the item is not truthy |
0xA7 | GOTO | Unconditional jump to location specified by 2 byte argument |
0x83 | CALL_FUNCTION | Saves the current state of the caller in a frame and pushes it on the call stack, sets the code of the current frame to that of the function at index specified by 1 byte argument, pops argc items from the caller's operand stack and pushes them on the callee's operand stack, and begins executing called function |
0x84 | CALL_NATIVE | Looks up the function at index specified by 1 byte argument from the builtin function table, and executes it |
0x53 | RETURN_VALUE | Restores instruction pointer and state of the caller function, and pushes return value on the operand stack of the caller |
The Locks Editor is a minimal text editor made with tkinter, with which you can open, edit, save, and run locks files.
A file can be opened through the File -> Open
option. Note that opening a new file will override the current open file, so make sure it is saved first.
A file can be saved through the File -> Save
or File -> Save As
option, as a text(.txt) or a locks(.lks) file.
A currently open locks file can be run through Run -> Run
or Run -> Run (debug)
. Run
will run the program through the VM, and Run (debug)
will run the program through the tree walk interpreter.
A new terminal window will be opened on Windows for code execution. Note that running the program through the VM exits the terminal as soon as execution is complete, an input("")
can be added to the end of the locks program to stop this from happening.
On linux, the locks program will execute in the terminal that the editor was run from.
The AST can be visualized from the editor by selecting the Run -> Visualize AST
option. This will attempt to create and render a dot file. Any error or message is shown on a separate console window. On linux, the messages are shown on the same console that the editor was run from.
The font size and theme can be changed from Options -> Preferences
.
The default dark and light themes were inspired by the Cyberpunk 2077 and the Atom One light syntax theme. If you don't like either, switch to the notepad theme or create your own.
You can create your own theme files by following the format below and saving the json file in the editor/theme
folder. Note that (as of now) the editor does not check for the validity of the theme files.
Theme example: defaultDark.json
{
"name": "Default Dark",
"background": "#030d22",
"foreground": "#ffffff",
"selectBackground": "#35008b",
"inactiveselectbackground": "#310072",
"cursorColor": "#ee0077",
"keywords": "#ff2cf1",
"functionName": "#ffd400",
"strings": "#0ef3ff",
"comments": "#0098df"
}
Note: Syntax highlighting for multiline comments is disabled for now since it may cause other syntax highlighting problems. See known bugs.
Ctrl+o
: Open fileCtrl+s
: Save currently open fileTab
andShift+Tab
: Add/remove 4-space indent to selection or current line
The interpreter can be used to visualize the AST of an input program as a graph. This can be done as follows:
python locks-interpreter.py <path-to-locks-file> -g <write-dot-to-file>
For example:
python locks-interpreter.py examples/fibonacci.lks -g fib.dot
The interpreter will write the generated dot code to a file at the location specified by the argument (which must be a filename). It will then try to use the QuickChart GraphViz API to generate and get an svg file from the dot file, use chairo to convert it to a png file, and then show the image on a window through Pillow and Tkinter. Pillow, requests, and chairo will need to be installed separately using pip for this to work. If not installed, the dot file alone will be generated. The generated code can be pasted to GraphViz Online to render it.
The AST can also be visualized from the editor by selecting the Run -> Visualize AST
option.
-
The tree walk interpreter crashes when the lvalue of an assign statement tries to index a nested list.
var a = [1,2,[3,4],5] a[2][1] = 8; // Crash!!
This works in the VM, however.
-
If an operand of a comparision expression with is a function, it works fine in the VM. If either operand is a function, the tree walk interpreter throws a type error. If the left operand is a function and the result of the expression is assigned to a variable, the semantic analyser throws a type error.
fun a(){ print("hello"); } // works in the VM, but tree walk interpreter throws a type error println(a == a); // semantic analyzer throws a type error var l = a and 4;
-
The VM does nothing and continues with execution if there is a return statement outside a function. The tree walk interpreter throws a 'return outside function' syntax error.
-
Multiline comments are not correctly highlighted in the Editor. They only work when
/**/
is typed in first, and then the comment is written. When highlighted correctly, their foreground doesn't change even on change of theme. For now, syntax highlighting for multiline comments will be disabled.