The hilvl programming language
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.
Example of hilvl
(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
Structure of the hilvl language
Service, action and argument
All statements in hilvl are invocations of single argument actions that are part of some service:
Service action argument
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)
It is all services
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
2 is the service,
+ is the action and
40 is the argument. This action returns a new service
There are only 8 reserved symbols in hilvl:
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
@ 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.
Creating a service
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
Importance of parantheses
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
@ set myVar = (2 + 40) // This is correct
Intendation and lists
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
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
, 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.
Services and actions provided out-of-the-box
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)
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")
Variables, scope and evaluation
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]
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
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.
Advanced examples of hilvl
@ 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
Scope and higher-order programming
@ 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
How to run hilvl
To run a file with hilvl code:
node src/hl.js myFile.hl
To run all tests:
To run HiTTP web framework:
node src/HiTTP.js examples/todo-webapp/backend.hl