5. Control structures

A few language constructs are available in HSL in order to control program flow.

5.1. echo

The echo statement will convert the expression to a string and print its value.

echo expression ;
echo "hello world";

5.2. if

One of the most basic control structures in HSL is the if statement, it allows conditional control flow. if statements check the expression for truthiness.

if (condition)
        statement

In addition to if there is also an else branch available; executed if the expression yields false

if (condition)
        statements
else
        statements
if (true) {
        echo "statement is true";
}

5.2.1. Ternary operator

The ternary operator is an expression and allows expression branching, if the if_true_expression is omitted (Elvis operator) then the value of expression is used if it tests positive for truthiness, also the expression is not re-evaluated.

expression ? if_true_expression : if_false_expression
expression ? : if_false_expression

The ternary operator is right-associative with makes them stackable like if-else statements.

$var = isset($arg) ? $arg : "default value";

5.2.2. Null coalescing operator

The null coalescing operator is an expression and allows expression branching, if the variable tests positive for isset() and is not a value of None (null) it is used. The expression which makes up the variable is not re-evaluated. There is also a nullish coalescing assigment operator.

variable ?? if_false_expression

The null coalescing operator is right-associative with makes them stackable like if-else statements.

$data = json_decode(...);
$var = $data["settings"] ?? "default value";

5.3. for

for statements allows conditional loops.

for ( [initialization] ; [condition] ; [final-expression] )
        statements
for ($i = 0; $i < 10; $i++) {
        echo $i;
}

5.3.1. break

break will abort the loop iteration of a for, foreach, while and forever loop and also the execution of switch statements.

5.3.2. continue

continue will abort the current loop iteration of a for, foreach, while and forever loop, and restart on the next iteration.

5.4. foreach

foreach loops allows iteration on array values and Iterator to execute the same statements multiple times.

foreach (expression as $val)
        statements

foreach (expression as $key => $val)
        statements
foreach (["Apple", "Banana", "Orange"] as $fruit) {
        echo $fruit;
}

5.4.1. break

break will abort the loop iteration of a for, foreach, while and forever loop and also the execution of switch statements.

5.4.2. continue

continue will abort the current loop iteration of a for, foreach, while and forever loop, and restart on the next iteration.

5.5. while

while statements allows conditional loops.

while ( expression )
        statements
$i = 0;
while ($i < 10) {
        echo $i;
        $i += 1;
}

5.5.1. break

break will abort the loop iteration of a for, foreach, while and forever loop and also the execution of switch statements.

5.5.2. continue

continue will abort the current loop iteration of a for, foreach, while and forever loop, and restart on the next iteration.

5.6. forever

forever statements allows indefinite loops.

forever
        statements
$i = 0;
forever {
        echo $i;
        $i += 1;
}

5.6.1. break

break will abort the loop iteration of a for, foreach, while and forever loop and also the execution of switch statements.

5.6.2. continue

continue will abort the current loop iteration of a for, foreach, while and forever loop, and restart on the next iteration.

5.7. switch

switch statements are in many ways similar to nested if-else statements. case expressions are compared to the switch expression until a match is found. If no match is found, and a default label exists, it will be executed.

switch (expression) {
        case expression:
                statements
        break;
        case expression:
                statements
        break;
        default:
                statements
        break;
}

If executing a statement and break is omitted the control flow will fall-through to the next statement.

5.8. match

match expressions are in many ways similar to switch statements, however match is an expression and must result in a value. If no match, and no optional default label exists an exception will be thrown. Each match expressions may include multiple expression to be compared to seperated by a comma.

$variable = match (expression) {
        compare-expression => value-expression;
        compare-expression, compare-expression => value-expression;
        default => value-expression;
}
echo match (5) {
        0, 2, 4, 6, 8 => "even",
        1, 3, 5, 7, 9 => "odd"
};

5.9. include

The include statement allows code to be structured in logical modules and shared between different scripts. The include path can be any of the supported file path formats (file:X where file: is implicit). include’s file name argument do not support variable interpolation nor expression since the include needs to be resolved at compile time. The statements in the included file are included in-place (replacing the include statement itself).

include string ;
include "file:1";
include "1";

Note

The same file may be included multiple times. However cyclic inclusion is not permitted.

5.9.1. include_once

The include_once keyword will only include the file if it hasn’t been included before.

include_once string ;

5.10. import

The import statement allows code to be structured in logical modules and shared between different scripts very much like include_once with the difference that all symbols which should be used in the calling script has to be explicitly imported. Also instead of running the imported code directly it is executed in a seperate context (with its own function and variable symbol table) referred to as “the module’s global scope”. If a file is imported multiple times (regardless of the symbols imported) its code will only be executed once (a behaviour which could be used to initialize global state), very much like include_once would behave. All symbols in a module’s symbol table is exported (by default), that include symbols which the module itself has imported from another module (a.k.a forwarding imports). An import can not be conditionally and must be defined unconditionally in the script (that usually means at the top of a script).

import { symbol [ as symbol ] [ , ... ] } from string;
import * as symbol from string;
import variable from string [ with [ options ] ];
import { foo, bar as baz, $x as $y } from "module";
import { $x as $y } from "module";

Note

  • The same file may be imported multiple times, but it will only be executed once. However cyclic inclusion is not permitted.

  • The path for the module may point to a folder, if so the file main.hsl in that folder is used.

  • The special syntax ./file.hsl refer to a file in the same folder as the current file.

5.10.1. variables

A variable in the module’s global scope may be imported into the global scope. An imported variables is imported by reference (and not by value), hence all changes to the variable in the module will be reflected by the imported variable. An import statement is not allowed to overwrite variables in the local scope (if a conflict occures, it should be imported under another name).

import { $x, $y as $z } from "module";

5.10.2. functions

A function in the module’s global scope may be imported into the global scope. An imported function (when executed) is executed in the module’s global scope. Hence, the global keyword imports from the module’s global context.

import { v1, v2, v2 as vLatest } from "module";

5.10.3. wildcard

Wildcard imports (*) allows you to import all variables and functions from a module to a namespace (static class).

import * as foo from "foo";
foo::bar();
echo foo::$x;

5.10.4. data

All content in a module/file can be imported as a variable using different import methods at compile time (chosen by file extensions), this has the benefit of doing the data import once and the data shared across all executions.

import $config from "config.json";
import $lookup from "lookup.csv";
import $data from "data.txt" with [ "type" => "array" ];

Currently there are data imports for the following file extensions.

Extension/Loader

Transformation

Import type

json

json_decode

any

yaml

yaml_decode

any

csv

csv_decode

array

txt

str_split

string/array/Set

crt

X509::String

array of X509

eml

MailMessage::String

MailMessage

key

*_privatekey

Privatekey resource

While there are a lot of file extensions that do not have a loader (in the supported list above), it’s still possible to import any file using the syntax of loader!file.ext to override the selection of loader by file extension for that file (eg. txt!logo.png or json!config.cfg).

The .json import behaviour can not be changed but default is to allow comments.

The .yaml import behaviour can be changed with the following options.

  • types (array) Add support for the following predefined “tag” imports.

Type

Transformation

Import type

Map

Map()

Map

Set

Set()

Set

Regex

pcre_compile()

Regex resource

PrivateKey

*_privatekey()

Privatekey resource

X509

X509::String()

X509

MailMessage

MailMessage::String()

MailMessage

A sample import with types could look like this

import $data from "myfile.yaml" with ["types" => ["Set", "Map", "Regex"]];

With the YAML tags defined as the following

my-set: !Set
  - item1
  - item2
  - item3
my-map: !Map
  item1: !Regex /^pattern1$/
  item2: !Regex /^pattern2$/

The .csv import behaviour can be changed with the following options.

  • delimiter (string) The format separator. The default is ,.

  • header (boolean) If the CSV data includes a header. The default is true.

  • schema (array) A csv_decode() compatible CSV schema.

The .txt import behaviour can be changed with the following options.

  • type (string) Import the file line by line as an array (array type) or set (Set type) (without the CRLF or LF delimiter). The default is string.

The .crt import behaviour can be changed with the following options.

  • resource (boolean) Import the certificate as a X509Resource instead of X509 object. The default is false.

The .eml import behaviour can not be changed.

The .key import behaviour can not be changed.

Warning

When import object such as Map, Set or MailMessage. Do not modify these objects. As these objects are shared between all script execution. And this object and operations are not thread-safe.

5.10.4.1. wildcard file imports

For all data imports, glob or sometimes called fnmatch patterns are supported. This allows you to import multiple resources into the same variable (supporting both id and file:// matching). The resulting import will be an array where the data index on the file name.

import $certs from "file://keys/*.crt";
// ["file://keys/1.crt" => X509, "file://keys/2.crt" => X509, ...]

5.11. function

It’s possible to write new functions in HSL, and also to override builtin functions. A function may take any number of arguments and return a value using the return statement. If non-variadic arguments are specified, the number of argument given by the caller must match the number of required arguments in the function definition.

function funcname() {
        return expression;
}
function funcname($arg1, $arg2) {
        return expression;
}
function funcname(...$argv) {
        return expression;
}

Warning

Recursion is not allowed.

5.11.1. Named functions

A function may be named (in order to be callable by its name) according to the regular expression pattern [a-zA-Z_]+[a-zA-Z0-9_]* with the exception of reserved keywords. In order to prevent naming conflicts in the future with added reserved keywords; it may be a good idea to prefix the function name with a unique identifier like halon_func.

and array as barrier break builtin cache case class closure constructor continue default echo else false for foreach forever from function global if import include include_once isset match not none object or private readonly return switch true unset while with

You should avoid using keywords available in other general purpose languages and they may be added in the future. That includes keywords such as for, this, protected, public etc.

5.11.1.1. Function scope

Named functions are scoped either in the global scope (if not defined inside another function) or function scoped (a nested scope, may access functions in the previous scope). They are unconditionally registered at compile-time (control flow is not taken into consideration). Hence it doesn’t matter where in the scope it’s defined (eg. before or after it’s being called).

funcname("World");
function funcname($name) {
        echo "Hello $name";
}

Note

Named functions are “hoisted”.

5.11.2. Anonymous functions

The syntax for anonymous functions are the same as for named functions, with the exception that the function name is omitted. Hence they must be called by their value and not by name.

function (argument-list) {
        return expression;
};
$variable = function ($name) {
        echo "Hello $name";
};
$variable("World");

Note

An anonymous function may be used as an immediately-invoked function expression (IIFE), meaning it may be invoked directly.

echo function ($name) {
        return "Hello $name";
}("World");

5.11.3. Closure functions

The difference between an anonymous function and a closure function is that a closure function may capture (close over) the environment in which it is created. An anonymous function can be converted to a closure by adding the closure keyword followed by a capture list after the function argument list. These variables are captured by reference from the parent scope (function or global) in which they are created.

function (argument-list) closure (variable-list) {
        return expression;
};

Most languages which implement closures capture (closes over) the entire scope (doesn’t use the concept of a capture list). HSL does not with the reasoning that all variables are function local; if the entire scope were to be closed over ambiguities could easily arise, and secondly it allows the developer to explicitly state the intention of the code.

function makeCounter() {
        $n = 0;
        return [
                "inc" => function () closure ($n) { $n += 1; },
                "get" => function () closure ($n) { return $n; },
        ];
}
$counter1 = makeCounter();
$counter2 = makeCounter();

$counter1["inc"]();

echo $counter1["get"](); // 1
echo $counter2["get"](); // still 0, $counter2 hasn't been updated

Note

This feature is similar to the PHP implementation of closures (use) however HSL’s closure statement captures by reference.

In order to capture by value, the following immediately-invoked function expression (IIFE) pattern may be used.

$i = 3;
$f = function ($i) { return function () closure ($i) { return $i * $i; }; } ($i);
$i = 10;
echo $f(); // 3 * 3 = 9

5.11.4. return

The return statement return a value from a function. If the expression is omitted a value of none is returned.

function funcname() {
        return [ expression ];
}
function funcname() {
        return 42;
}

Note

If the return statement is omitted and execution reached the end of the function, a value of none is returned. This is fine if the function is a void function.

5.11.5. Default argument

Formal parameters may be initialized with a default value if not given by the caller. Default values may only defined as trailing parameters in the function definition. Constant expressions which can be evaluated during compile-time may be used as default values (e.g. $a = 10 * 1024 and $a = []).

function funcname($arg1 = constant_expressions) {
        statements
}
function hello($name = "World") {
        return "Hello $name.";
}
echo hello(); // Hello World.
echo hello("You"); // Hello You.

5.11.6. Variadic function

Arbitrary-length argument lists are supported using the ...$argument syntax when declaring a function, the rest of the arguments which were not picked up by an other named argument will be added to the last variable as an array. This variable has to be defined at the end of the argument list.

function funcname($arg1, ...$argN) {
        statements
}
function avg(...$values) {
        $r = 0;
        foreach ($values as $v)
                $r += $v;
        return $r / length($values);
}

$values = [0, 5, 10, 15];
echo avg(...$values);

5.11.7. global

The global statement allows variables to be imported in to a local function scope (by reference). If the variable is not defined at the time of execution (of the global statement) it will simply be marked as “global” and if later assigned; written back to the global scope once the function returns. If the variable that is imported to the function scope already exists in the function scope an error will be raised. If an imported variable is read-only, it will be read-only in the function scope as well.

function funcname() {
        global $variable[, $variable [, ...]];
}
function Accept() {
        echo "Message accepted";
        builtin Accept();
}
Accept();

5.11.8. Function calling

5.11.8.1. Argument unpacking

Argument unpacking make it possible to call a function with the arguments unpacked from an array at runtime, using the spread or splat operator (...). The calling rules still apply, the argument count must match. This make it easy to override function.

funcname(...expression)
$variable(...expression)

5.11.8.2. builtin

The builtin statement allows you to explicitly call the builtin version of an overridden function.

builtin funcname()
builtin funcname
function length($str) {
        echo "length called with $str";
        return builtin length($str);
}

echo length("hello");

5.12. function*

The function* statement declares a generator function, which returns a generator object. A generator object is an instance of a function that may yield or return multiple values. Generator functions support most of the function features available (eg. anonymous functions, default argument etc.). This manual may not be the best place to learn about generator functions in general as it more or less only describes our implementation of them.

function* name() {
        yield or return
}

5.12.1. generator object

A generator object (Iterator) is return when a generator function is called. It has one next method which invokes or resumes the generator function and returns the value of the next yield or return statement. If yield is used the function is suspended and the value from the yield statement is return. If return is used, the value is returned and the generator is finished (it may not be resumed).

  • value (any) The value returned from the generator function

  • done (boolean) The current state of the generator function

function* infinity() {
        $i = 1;
        while (true)
                yield $i++;
}
$x = infinity();
echo $x->next(); // ["value"=>1,"done"=>false]
echo $x->next(); // ["value"=>2,"done"=>false]

Generator objects may be used with the foreach statement to iterate all its values, then the value will be the iteration variable of the foreach.

function* counter($max) {
        $i = 1;
        while ($i <= $max)
                yield $i++;
}
$x = counter(5);
foreach ($x as $i)
        echo $i;

5.12.2. yield

The yield statement may be used to return multiple values from a generator function. If return is used within a generator function instead of yield the generator function is done and cannot be resumed with the next method.

function* countdown() {
        yield 3;
        yield 2;
        return 1;
        yield 0;
}
$x = countdown();
echo $x->next(); // ["value"=>3,"done"=>false]
echo $x->next(); // ["value"=>2,"done"=>false]
echo $x->next(); // ["value"=>1,"done"=>true]
echo $x->next(); // ["value"=>,"done"=>true]

When the yield keyword is used as a expression, its value will be the generator objects next function argument. It may still yield a value.

function* hello() {
        while (true)
            echo "Hello " . yield;
}
$x = hello();
$x->next(); // needed by pattern
$x->next("Alice");
$x->next("Bob");

5.13. Exceptions

The functionality of throwing and catching exceptions provide a mechanism to disrupts the normal execution flow on eg. error conditions.

5.13.1. try

try {
        code
} catch (variable) {
        code
}

In order to catch an exception, it is has to be thrown within a try/catch code block. When a exception is thrown the remaining code in the try statement is not execution and the execution flow is moved to the first statement of the catch code block. Exception are catched at the first catch statement in the callstack, and are not propegated unless re-thrown. If and exception is thrown outside of a try/catch code block it will result in a script error.

try {
        echo "Hello";
        throw Exception("Error");
        echo "You"; // Never executed
} catch ($e) {
        echo "World";
}

5.13.2. catch

The caught exception is available in the variable specificed in the catch statement.

try {
        // throw here
} catch ($e) {
        echo $e;
}

5.13.3. throw

Any value type may be thrown. Built-in runtime exceptions are instances of the Exception class.

try {
        throw "Hello World";
} catch ($e) {
        echo $e;
}

Note

If throwing custom classes, any available toString() method may be called when logging the error.

5.14. class

The class statement can be used to declare new types of classes. The class-name must be a valid function name. In order to create a new instance of a class (object) call the class by name using the function calling convention. Class instances (objects) are not copy on write and all copies reference the same object. The default visibility of class members are public.

class class-name
{
        constructor() {}

        $variable = initial-value;
        function function-name() {}

        private $variable = initial-value;
        private function-name() {};

        readonly $variable = initial-value;

        static $variable = initial-value;
        static function function-name() {}

        private static $variable = initial-value;
        private static function function-name() {}
}

Note

Names of functions and variables may not conflict, as it will cause a compile error.

5.14.1. constructor

The constructor (function) is a special function declared inside the class statement. This function (if it exist) is called when an object is created, all arguments from the class-name calling is passed to the constructor function. The constructor function supports all features of function calling (such as default arguments). The constructor is usually used to initialize object instance variables on the special $this variable.

class Foo
{
        constructor($a, $b = 5) { $this->a = $a; }
}
$x = Foo(5);

Note

There is no destructor. Objects are destructed (garbage collected) when they aren’t referenced by anyone.

5.14.2. Instance

An instance of a class is created by calling the name of the class (hence calling the constructor). This will create a special $this variable bound to the object. Property and method access is done with the property access operator (->) or subscript operator ([]).

5.14.2.1. $this

The $this variable is a private instance variable that can only be used within a class function and is used to access the instance methods and variables (private and public). This variable is iterable using the foreach structure. If returned from a class function it will reference the public interface of the instance.

class Foo
{
        $value = 0;

        // serialize data members...
        function serialize() {
                $data = [];
                foreach ($this as $k => $v)
                        if (!is_function($v))
                                $data[$k] = $v;
                return $data;
        }

        // unserialize data members
        function unserialize($data) {
                foreach ($data as $k => $v)
                        $this[$k] = $v;
                return $this;
        }
}

$x = Foo();
$x->value = 123;

$export = json_decode(json_encode($x->serialize()));

$y = Foo()->unserialize($export);
echo $y->value;

5.14.2.2. variables

An instance variable is either defined in the class body or created on the $this object (variable) in the constructor function. At any time, new properites may be added and removed on the $this object.

class Foo
{
        $y = 5;
        constructor() { $this->x = 5; }
}
$x = Foo();
echo $x->x;
echo $x->y;

5.14.2.3. functions

A instance function is a function declared in a class statement and is only available on object instances. On execution it has access to the object’s $this variable.

class Foo
{
        function setX() { $this->x = 5; }
}
$x = Foo();
echo $x->setX();

5.14.3. Static

A static function or variable is not bound to a class instance instead they are only scoped by the class name using the scope resolution operator (::). Static members are not available on instance objects.

5.14.3.1. variables

A static variable is declared within a class statement using the static keyword. A static variable is namespaced to the scope of the class name and it’s initialized at compile time (but can be updated and used at runtime). A static variable can only be initialized to a constant expressions which can be evaluated during compile-time.

class Foo
{
        static $x = 10;
}
echo Foo::$x;

5.14.3.2. functions

A static function is declared within a class statement using the static keyword. A static function is namespaced to the scope of the class name. On execution it does not has access to a $this variable. Instead to hold state, a static function usually use static class variables.

class Foo
{
        static $x = 10;
        static function getX() { return Foo::$x; }
}
echo Foo::getX();

5.14.4. Visibility

The default visibility of class members are public. However both instance- and static -variables and functions can be declared as private.

5.14.4.1. private

Variables and functions may be declared as private, in which case they can only be accessed from within other function on the same instance or class.

class Foo
{
        function publicAPI() { $this->do(); }
        private function do() { }

        static function publicAPI2() { Foo::do2(); }
        private static function do2() { }
}
$x = Foo();
$x->publicAPI();
Foo::publicAPI2();

5.14.5. Permissions

The default permissions of public class variable allows for read and write access.

5.14.5.1. readonly

Variables may be declared as readonly, in which case they can only be written to from within function on the same instance.

class Foo
{
        readonly $count = 0;

        function inc() { $this->count += 1; }
}
$x = Foo();
$x->inc();
echo $x->count; // 1
$x->count = 2; // Error

5.15. cache

The cache statement can be prepended to any named function call. If the same call is done and the result is already in its cache the function will not be executed again, instead the previous result will be used. The cache take the function name and argument values into account when caching. In case of concurrent cache misses, the consecutive request will wait for the first to complete and use its result.

cache [ cache-option [, cache-option [, ...]]] [builtin] funcname()

The following cache options are available.

  • ttl (number) Time to Live (TTL) in seconds for the cache entry if added to the cache during the call. The default time is 60 seconds.

  • ttl_override (array) An associative array where the key is the return value and the value is the overridden ttl to be used.

  • ttl_function (function) A function taking one argument (the function’s return value) and returning the ttl to be used.

  • update_function (function) A function called at cache updates; taking two arguments (the old and new value) and returning the value to be used and cached.

  • insert_function (function) A function called at cache insert; taking one arguments (the new value) and returning the value to be used and cached.

  • argv_filter (array) A list of argument indexes (starting at 1) which should make this cache entry unique. The default is to use all arguments.

  • force (boolean) Force a cache-miss. The default is false.

  • size (number) The size of the cache (a cache is namespace + function-name). The default is 32.

  • namespace (string) Custom namespace so that multiple caches can be created per function name. The default is an empty string.

  • lru (boolean) If the cache is full and a cache-miss occur it will remove the Least Recently Used (LRU) entry in order to be able to store the new entry. The default is true.

// cache both the json_decode() and http() request
function json_decode_and_http(...$args) {
            return json_decode(http(...$args));
}
$list = cache [] json_decode_and_http("http://api.example.com/v1/get/list");

The _function arguments will be called in the following order.

  • On initial cache call
    1. ttl_function

    2. insert_function

  • On cache update
    1. ttl_function

    2. update_function

Warning

Not all functions should be cached. If calls cannot be distinguished by their arguments or if they have side-effects (like Defer), bad things will happen.

// Defer messages on Sundays
if (cache [] strftime("%u") == "7")  // The same (and incorrect) day will be used for all days messages
    cache [] Defer();                // if Sunday, Defer may still on happen once for one single message...
Accept();                            // ...and all other messages will be delivered.

Note

By default (if not distinguish by namespace), all cached calls to the same function name share the same cache bucket, consequently the cache statement with the smallest size set the effective max size for that cache. It’s recommended to use different namespaces for unrelated function calls.

Note

The cache is not cleared automatically if the configuration is reloaded.

5.16. barrier

A barrier is named mutually exclusive scope, only one execution is allowed to enter the same named scope (applies to all thread). Waiters are queued for execution in random order. Optionally with every barrier comes a shared variable (shared memory) which data is shared among executions. Barries share the same memory and lock as the memory functions.

barrier statement {
        statements
}
barrier statement => variable {
        statements
}
barrier "counter" => $var {
        $var = isset($var) ? $var : 0;
        echo $var;
        $var += 1;
}