/blink

A simple programming language to learn how to design and implement a programming language.

Primary LanguageJavaScript

Blink Programming Language

Blink is a tiny programming language whose sole purpose is to learn how to design and implement a programming language. Blink is an interpreted class-based, object-oriented programming language featuring a strong static type system. Its syntax and semantics are inspired by the programming languages Scala, Clojure, Kotlin and Rust.

This repository contains the tokenizer, the parser, the type checker, the interpreter and the standard library.

Getting starting

How to setup

Blink is implemented in ES6 and the interpreter requires Node.js >= 4.2.2 to run.

With Node.js installed, type the following commands in a prompt to setup Blink.

  1. $ git clone https://github.com/andela-ftchirou/blink.git
  2. $ cd blink
  3. $ npm install

How to build

Run npm run build to build.

How to run

Run npm run repl to start an interactive shell for writing Blink programs. Just type your expressions in the interpreter to have them evaluated.

First steps with Blink

How to use the Blink interpreter

$ npm run repl

Welcome to Blink 0.0.1
Type in expressions to have them evaluated.
Type :quit to quit.


blink> 

Type an expression e.g 1 + 2 and hit Enter

blink> 1 + 2

The interpreter should respond with

res0: Int = 3

  • res0 is a name you can use to refer to this result in later expressions
  • Int is the type of the result
  • 3 is the value of the result.

Multiline expressions

Expressions can span multiple lines in the interpreter. When pressing Enter, if Blink detects that the expression typed is not yet complete, it will allow you to continue the expression on the next line. Once a complete expression is detected, Blink will automatically evaluate it.

Welcome to Blink 0.0.1
Type in expressions to have them evaluated.
Type :quit to quit.


blink> 1 +
      | 2 +
      | 3 +
      | 4 +
      | 5
res1: Int = 15

Cancel the current expression

Type Enter twice (consecutively) to cancel the current expression and start a new one.

blink> 1 +
      | 
      | 
Two blank lines typed. Starting a new expression.

Load external files

You can define your code in externals files and load them into the interpreter using the :load command.

blink> :load <absoluteFilePath1> [ <absoluteFilePath2>, ...]

What to do in the interpreter

In Blink, everything (a part from global variables, functions and classes which are definitions) is an expression. You can type expressions in the interpreter to have them evaluated.

Evaluate literals

Numbers, booleans and strings are all literal expressions and they evaluate to themselves in the interpreter.

blink> 42
res0: Int = 42

blink> 3.14
res1: Double = 3.14

blink> true
res2: Bool = true

blink> "Hello, World!"
res3: String = "Hello, World!"

Compute

You can use the interpreter as a calculator and compute mathematical expressions involving the following operators +, -, *, / and %.

blink> 7 - 4 + 2
res1: Int = 5

Print

Use Console.println() to print something in the interpreter.

blink> Console.println("Hello, World!")
Hello, World!

Declare a local variable with let

Block-scoped variables can be defined using a let expression

blink> let message: String = "Hello, World!" in {
      |     Console.println(message)
      | }
Hello, World!

In this example, we defined a variable named message of type String initialized to "Hello, World!. The part of the expression after the in keyword is the body of the let expression. The variable message is accessible only inside the body of the let.

A let expression evaluates to the value of the last expression in its body.

Omit the type of the variable

If a variable is initialized at its declaration, then its type can be omitted. Blink is able to infer the correct type of a variable according to its value.

blink> let message = "Hello, World!" in {
      |     Console.println(message)
      | }
Hello, World!
Multiple variables

To declare multiple variables at once, separate them with commas.

blink> let a = 2, b = 3 in {
      |     a + b
      | }
res1: Int = 5

The curly braces around the body can be omitted if the body contains only one expression

blink> let a = 2, b = 3 in a + b
res3: Int = 5

Decide with if

You can use an if expression to execute one or other expression according to a condition. The condition must evaluate to a Bool value.

blink> if (true) {
      |     "true"
      | } else {
      |     "false"
      | }
res7: String = "true"

If the body of the if or else branch is made of only one expression, the curly braces can be omitted.

blink> if (true) "true" else "false"
res8: String = "true"

Loop with while

A while expression is used to execute one or more expressions as long as a condition holds true.

blink> let i = 1 in {
      |     while (i <= 10) {
      |         Console.println(i)
      |         i += 1
      |     }
      | }
1
2
3
4
5
6
7
8
9
10

Define a global variable

Global variables are defined using the var keyword and are accessible in all expressions in the interpreter.

blink> var message: String = "Hello, World!"
message: String = Hello, World!

As with let expressions, indicating the type of the variable is optional when the variable is initialized at its definition.

blink> var message = "Hello, World!"
message: String = Hello, World!

Define a function

Functions are declared using the func keyword.

blink> func add(a: Int, b: Int): Int = {
      |     a + b
      | }
add(a: Int, b: Int): Int

After the func keyword, comes the name of the function, the list of parameters separated by commas and enclosed in parentheses (the type of each parameter must be explicitely provided, preceded by a :), a :, the return type of the function, an = and the body of the function which is a list of expressions enclosed in curly braces.

Once a function is defined, you can call it in the traditional way.

blink> add(2, 3)
res4: Int = 5
Unit functions

If a function does not return any value, its return type must be Unit. However, the Unit return type declaration is optional, you can then write methods like

blink> func greet() = {
      |     Console.println("Hello, World!")
      | }
greet()
Single-Expression functions

If the body of the function is made up of only one expression, the curly braces can be omitted.

blink> func add(a: Int, b: Int): Int = a + b
add(a: Int, b: Int): Int
No return keyword

There is no return keyword in Blink. The last expression of the body of a function is the return value of the function.

Define a class

Classes in Blink are declared using the class keyword.

blink> class Person {
      | }
defined class Person
Constructor

A class in Blink can only have one constructor which is part of the class header. To define a constructor, add a list of parameters enclosed in parentheses to the name of the class.

blink> class Person(firstname: String, lastname: String) {
      | }
defined class Person

Objects are then created using the new keyword

blink> new Person("John", "Doe")
res6: Person = Person@8
Properties

Class properties are declared with the var keyword

blink> class Person {
      |     var firstname: String
      | 
      |     var lastname: String
      | 
      |     var age: Int
      | }
defined class Person

Properties can be initialized at declaration. The initialization expression of a property will be evaluated when the object is being created.

Properties are private

Properties in Blink are private and they cannot be made public. If you need to access a property outside of a class, you will need to create a getter and/or a setter for it.

Functions

A class can have functions. Functions are declared as normal functions with the func keyword.

blink> class Person(firstname: String, lastname: String) {
      |     var age: Int = 0
      | 
      |     func firstname(): String = {
      |         firstname
      |     }
      | 
      |     func setFirstname(name: String) = {
      |         firstname = name
      |     }
      | 
      |     // ...
      | }
defined class Person

Functions can then be called on an object using the . operator.

blink> var person = new Person("John", "Doe")
person: Person = Person@this

blink> person.firstname()
res7: String = "John"

Functions are public by default

Functions in Blink are public by default. To make a function private, add the private modifier to its declaration.

blink> class Person {
      |    private func age(): Int = ...
      |}
Inheritance

A class can inherit another class with the extends keyword.

blink> class Employee(firstname: String, lastname: String, company: String) extends Person(firstname, lastname) {
      |     func company(): String = company
      | 
      |     func setCompany(c: String) = company = c
      | }
defined class Employee

blink> var employee = new Employee("John", "Doe", "ACME")
employee: Employee = Employee@this

blink> employee.firstname()
res8: String = "John"

blink> employee.company()
res9: String = "ACME"

When specifying the superclass, you must pass in the parameters required by the constructor of the superclass.

Object class

By default, all classes inherit from the Object class.

Override functions

To override a superclass function, use the override modifier.

blink> class Person(firstname: String, lastname: String) {
      |     override func toString(): String = "Person(" + firstname + ", " + lastname + ")"
      | }
defined class Person

blink> new Person("John", "Doe")
res9: Person = Person(John, Doe)

toString()

The interpreter uses toString() to display values in the REPL. So, it's always a good idea to override toString() in your classes to have a more friendly and accurate representation of your values instead of the default <className>@<address> .

blink> new Person("John", "Doe")
res9: Person = Person(John, Doe)

instead of

blink> new Person("John", "Doe")
res10: Person = Person@12

Next steps

Have a look in the samples directory to learn more about the inner details of writing Blink programs.

Domo!