Skip to content

Python implementation of locks, which is an imperative, dynamically typed, procedure oriented scripting language based on the lox programming language.

Notifications You must be signed in to change notification settings

1Hibiki1/locks-py

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Locks

[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.

Contents

Usage

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 Locks language

IO

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)

Comments

Locks supports single line and multi-line C-style comments. Multi-line comments cannot be nested.

// single line comment
/*
    Multi-line comment
*/

Keywords and Identifiers

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!

Datatypes

Locks supports the following datatypes:

  • Nil: Used to define a null value, denoted by the nil keyword
  • Number: 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 be true or false
  • 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

Variables are declared using the var keyword.

var a;  // declare
a = 5;  // assign
var b = 5;  // declare and assign

Statements

Locks supports the following statements (apart from loops, return, continue and break statements):

Expression Statement

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)

  • ==: Returns true if left operand is equal to right operand, otherwise false

  • !=: Returns true if left operand is not equal to right operand, otherwise false

  • <: Returns true if left operand is less than right operand, otherwise false. Both operands must be numbers

  • >: Returns true if left operand is equal to right operand, otherwise false. Both operands must be numbers

  • <=: Returns true if left operand is equal to right operand, otherwise false. Both operands must be numbers

  • >=: Returns true if left operand is equal to right operand, otherwise false. Both operands must be numbers

  • and: Returns true if both left and right operands are truthy, otherwise false

  • or: Returns true if either the left or the right operand is truthy, otherwise false

  • !: Performs a logical not on its operand

Assign Statement

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;

If Statement

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;
}

Loops

While loop

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;
}

For loop

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

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

Builtin functions

IO functions

  • print: Accepts 1 argument and prints it to stdout
  • println: Accepts 1 argument and prints it to stdout, with newline
  • input: Accepts 1 argument and prints it to stdout, and accepts input from stdin

For usage of these functions, check IO.

String and Array functions

String
  • isinteger: Accepts a string as argument, returns true if the string is a valid integer, false otherwise
Both
  • len: Accepts a string or an array as argument, and returns its length

Type conversion

  • int: Accepts 1 argument, converts it to an integer, and returns it
  • str: Accepts 1 argument, converts it to a string, and returns it

The Locks VM

The Locks VM is inspired by the JVM and the Python VM.

Byte code Format

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));

Constants Pool

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: double representation

For example, 3.14 will be stored as 0x00 0x20 0x00 0x00 0x00 0x00 0x01 0x3a.

Opcodes

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

Editor

The Locks Editor is a minimal text editor made with tkinter, with which you can open, edit, save, and run locks files.

Opening 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.

Saving files

A file can be saved through the File -> Save or File -> Save As option, as a text(.txt) or a locks(.lks) file.

Running Locks programs

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.

Visualizing The AST from the Editor

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.

Customizing

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.

Keyboard shortcuts

  • Ctrl+o: Open file
  • Ctrl+s: Save currently open file
  • Tab and Shift+Tab: Add/remove 4-space indent to selection or current line

Visualizing The AST

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.

Known Bugs

  • 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.

About

Python implementation of locks, which is an imperative, dynamically typed, procedure oriented scripting language based on the lox programming language.

Topics

Resources

Stars

Watchers

Forks

Languages