Hilvl is a programming language that is versatile but with a very small syntax. All code in hilvl are single-argument invocations of actions that belong to services.
Services are the fundamental building blocks of a hilvl program. And the name hilvl reflects how this is a higher level of abstraction than objects and functions.
(If you don't get this, skip to the chapters below.)
@ var foo = 42
@ var bar = (2 + 40)
@ . foo == (@ . bar) then
@ set foo = 0
@ var myList =
1
2
3
@ . myList loop
@ set foo = (@ . foo + (@ . element))
@ var myMap =
Map of
"firstname" , "Ola"
"lastname" , "Nordmann"
@ var MyService :=
@ var myAction :
@ . argument + 10
MyService myAction (@ . foo) // foo is now 6, and this returns 16
All statements in hilvl are invocations of single argument actions that are part of some service:
Service action argument
For instance:
TextUtil makeUppercase "hello"
ScoreKeeper add 10
The magic happens when an action returns another service that can be called in a chain:
SandwichMaker makeNewSandwich "cheese" addSome "tomato"
Services can of course be arguments to other actions:
Chooser makeChoice (StrategyMaker mustBeLargerThan 10)
Everything is a service in hilvl, even strings and numbers. And action names can be anything. This means that even this is an ordinary action invocation:
2 + 40
Here, 2
is the service, +
is the action and 40
is the argument. This action returns a new service 42
There are only 8 reserved symbols in hilvl:
"
(
)
//
true
false
(numbers)
(whitespace)
The rest are services and actions defined by either the user or the system. This means that all other characters can be used when defining an API. (Allthough, you probably don't want to use the same name as a system service.)
Here is an example that uses unusal action and service names:
@ set myVariableName = 42
Here, @
is the service, set
is the action and myVariableName
is the argument. This returns a new service which has an action =
that is called with 42
as an argument. @
is actually a system service for handling scope. There is more about that later.
Since everything is a service, it is easy to make your own. The last statement of an action is its return value:
// Creating a new service with an action:
@ var MyService :=
@ var myAction :
@ var myVariable = (42 + (@ . argument))
@.myVariable // This is the return value
MyService myAction 1 // Using the service
//result: 43
There is no precedence in hilvl. This means that parantheses may be necessary to group arguments correctly:
@ set myVar = 2 + 40 // This fails during runtime!
This is wrong because the action named =
will take 2
as its argument, but the resulting service will not have an action named +
. The correct way is to group 2 + 40
to a single argument for the =
action:
@ set myVar = (2 + 40) // This is correct
Every action takes only 1 argument, but that argument can be a list. Whitespace intendation is used to declare a list:
MyUtil sort
4
2
9
3
Here, the argument is the list [4,2,9,3]
.
Why use whitespace for this you ask? The point is that if you make a list of statements, then you have a block of code. All code in hilvl are lists of statements that can be passed around and invoked by other code.
Here is an example:
KeyboardService onEvent
Player move "up"
Time increase 1
CollisionControl doCheck _
Here, the implementation of onEvent
may decide itself when to evaluate the statements in the argument.
If the argument is ommitted entirely, it is interpreted as an empty list:
// This is an empty list:
@ var emptyList =
// The same applies inside parantheses:
(MyService getValue) doSomething 42
To make the code more readable, a very simple shortcut is supported in the syntax:
foo.bar
is the same as foo. bar
is the same as foo . bar
foo,bar
is the same as foo, bar
is the same as foo , bar
This means that the .
and ,
are action names even though there are no space around them. This is only for these actions. All other services, actions and arguments must have spaces between them. This is because they can be called anything except the reserved symbols, and this in turn is why hilvl is very versatile and can used for implementing domain specific languages.
Syntactic sugar is considered to be evil, so this is the only sugar in hilvl.
The hilvl runtime provides several useful services in addition to the scope service. These are called system services because their functionality can not be made in hilvl by itself. Hilvl also comes with a standard library implemented in the language itself, providing some useful services.
"foo" + "bar" == "foobar"
"Hello!" length _ == 6
"Hello!" at 1 substringTo 4 == "ell"
"Hello !" at 6 insert "World" == "Hello World!"
"foo" == "bar" == false
1 + 2 - 3 == 0
10 > 4 == true == (4 < 10)
123 as string == "123"
@ var n = 0
10 until
// This will loop until n is 10
@ set n = (@.n + 1)
@.n
1 < 2 == true
false != true
@.n < 10 then
// This will run if n is lower than 10
MyService myAction
@ var myList =
40
41
42
@.myList get 1 == 41
@ var emptyList =
@.myList loop
// This will loop through every element in myList
@.emptyList push (@.element)
There is a convenient action ,
on strings, numbers and booleans. It returns a list containing the value and the argument:
1, 2 // This is the list [1,2]
"one", "two" // This is the list ["one","two"]
true, false // This is the list [true,false]
A clever trick is that the List service also has a ,
action that returns a new list with the argument added. So it is possible to chain this with several elements:
@ var myList = ("foo", "bar", "baz", "hello")
This is a simple key-value map.
@ var myPlayer =
Map of
"name", "Holger"
"score", 120
"alive", true
myPlayer put ("score", 121)
myPlayer get "name"
@ var emptyMap = (Map of)
The IO
service handles input and output to and from systems outside the hilvl runtime environment.
IO print "This will be printed in the console"
@ var fileContents = (IO readFile "myfolder/myfile.txt")
The Service action argument
structure and indentation based lists are combined with the scope system for great flexibility for the programmer.
Variables are created, changed and read by using the scope service @
:
@ var myVar // variable "myVar" is declared
@ set myVar = 42 // myVar is given a value
@ var myOtherVar = 10 // the variable service also has an = action for more consise code
@.myVar + (@.myOtherVar) // the values of myVar and myOtherVar are read and added together
//result: 52
All variables are saved in the same scope. But to add a new nested scope, there is an action named :=
on the variable service:
@ var myVar1 = 1
@ var myVar2 = 2
@ var myScope := // the two statements in the argument are now evaluated in a new scope:
@ var myVar1 = 10
@ set myVar2 = 20
// we place the variables in a list that is returned as the result:
@ var myList =
@.myVar1
@.myVar2
//result: [1, 20]
Notice how myVar1
kept its value because the change to 10
was done on a new variable with the same name in the inner scope. myVar2
on the other hand, was not redeclared in the inner scope, and its value was thus changed to 20
.
The scopes are nested, which means that if a variable is used, its value will be searched for upwards in all parent scopes.
After adding a new scope, the :=
action acts exactly like the =
action, and evalates the argument list. This means that any statements in the argument gets executed. And in the example above, this meant that the variables where changed.
But it is possible to set a value to a variable without evaluating the arguments. This is useful when we want to execute a block of code at a later time, or many times over. This is also the key mechanism for structuring code as services and actions.
It is done with the action :
:
@ var bar = 1
@ var foo : // the statement in the argument is not evaluated yet
@ set bar = 2
@ var barBefore = (@.bar)
@ foo // this invokes the foo action with an empty argument, and the code is evaluated
@ var barAfter = (@.bar)
@ var results =
@.barBefore
@.barAfter
//result: [1, 2]
If a list of statements is executed, the value of the last statement is returned from the action. All hilvl code are lists of statements, so this is why the last value is always the result in the examples.
@ var User1 :=
@ var age : 34
@ var name : "Bob"
@ var User2 :=
@ var age : 36
@ var name : "Alice"
@ var users =
@.User1
@.User2
@.users loop
IO print (@.element name _ + " " + (@.element age _ as string))
// Prints:
// "Bob 34"
// "Alice 36"
@ var fibonacci :
@ var scope : // A new scope is needed. Or else, the result variable is shared between the recursive calls
@ var result = (@.argument)
@.argument > 1 then
@ set result =
@ fibonacci (@.result - 1) + (@ fibonacci (@.result - 2))
@.result
@ scope (@.argument)
@ fibonacci 7
//result: 13
@ var foo = 10 // Variable in outer scope
@ var MyService :=
@ var myAction :
@ set foo = 42 // Variable in inner scope
@ var myFunction : (@.argument) // Saving argument without evaluating it
@ myFunction // Invoking the argument as an action
@ var bar =
MyService myAction (@.foo + 2) // Argument is evaluated before action is invocated
MyService myAction // Argument is evaluated on demand by the myAction implementation
@.foo + 2
MyService myAction
@ set foo = 50 // The inner scope is active during on demand evaluation
@.foo + 2
/*result
[12, 44, 52]
*/
@ var Please :=
@ var add :
@ var arg1 = (@.argument)
@.Please // Returning the service itself
@ var and :
@ var arg2 = (@.argument)
@.Please // Returning the service itself
@ var andThen :
@.arg1 + (@.arg2) + (@.argument)
Please add 42 and 50 andThen 100
//result: 192
To run a file with hilvl code: node src/hl.js myFile.hl
To run all tests: node tests/run-all-tests.js
To run HiTTP web framework: node src/HiTTP.js examples/todo-webapp/backend.hl