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.
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.
$ git clone https://github.com/ftchirou/blink.git
$ cd blink
$ npm install
Run npm run build
to build.
Run npm run repl
to start an interactive shell for writing Blink programs. Just type your expressions in the interpreter to have them evaluated.
$ 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 expressionsInt
is the type of the result3
is the value of the result.
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
Type Enter
twice (consecutively) to cancel the current expression and start a new one.
blink> 1 +
|
|
Two blank lines typed. Starting a new expression.
You can define your code in externals files and load them into the interpreter using the :load
command.
blink> :load <absoluteFilePath1> [ <absoluteFilePath2>, ...]
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.
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!"
You can use the interpreter as a calculator and compute mathematical expressions involving the following operators +
, -
, *
, /
and %
.
blink> 7 - 4 + 2
res1: Int = 5
Use Console.println()
to print something in the interpreter.
blink> Console.println("Hello, World!")
Hello, World!
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.
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!
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
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"
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
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!
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
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()
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
There is no return
keyword in Blink. The last expression of the body of a function is the return value of the function.
Classes in Blink are declared using the class
keyword.
blink> class Person {
| }
defined class Person
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
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.
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 = ...
|}
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.
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
Have a look in the samples directory to learn more about the inner details of writing Blink programs.
Domo!