Skip to content

Language Constructs

Gianmarco edited this page May 28, 2023 · 5 revisions

Introducing new identifiers

In order to introduce a new identifier, you can use the var keyword:

var num = 12
var str = "hi!"

You cannot modify a variable, but only define it. This means that once you have introduced a symbol, you cannot re-assign it with another value, but only create it anew:

var a = "test"
a = "test2"     // this is invalid

var a = "test2" // this is ok

Control statements

Harlock supports simple if/else control statements in the following form (the else bit is optional):

if bool condition { statements } else { statements }.

This is an example of actual valid control blocks:

if x < y { ret x } else { ret y }

if a * (2 - b) > c {
    ret "ok"
}

Notice that if/else statements are actually expression statements, so the following is valid:

var input = 1
var p = if input == 1 { "one" } else { "not one" }
print(p) // prints "one"

Functions

The harlock language does not offer a way to create custom data types. The only way for users to implement blocks that describe custom behavior is to define functions.

A function is an object in and of itself -- harlock has functions as first-class citizens. This means that functions can be passed as parameters to other functions or they can be returned from another function.

In order to create a function, you can use the fun keyword in the following way:

fun(comma-separated untyped arguments) { body }

To tie a function literal to an identifier, the var keyword can be used in the same way as for other data types:

var f = fun(x) {
   ret x
}

Notice that you can use the ret keyword to return from the function. Using the keyword followed by a value (e.g., ret 1) returns that value to the caller. If no ret statement is found at the end of a function body, the last value gets returned:

var inc = fun(x) {
  // This is equivalent to ret x+1
  x+1
}

Since functions are first-class citizens, they can be used within other language structures, both through an identifier if they have one, or anonymously as literals:

// Simple function tied to the 'f' identifier
var f = fun(x) {
   ret x
}

// We can put the function into an array through its identifier, alongside function literals
var arr = [f, fun(){}]

// Simple sum function
var sum = fun(x, y) {
  ret x + y
}

// Passing a function literal as a function input parameter:
var sum1 = [1, 2, 3].reduce(fun(x, y) {
  ret x + y
})

// Passing a function literal as a function input parameter, chaining calls:
var sum_doubles = [1, 2, 3].map(fun(x) {
  ret x * 2
}).reduce(fun(x, y) {
  ret x + y
})

// Passing a function through its identifier:
var sum2 = [1, 2, 3].reduce(sum)

Managing errors (experimental)

There are three kinds of errors in harlock:

  • parsing errors, which raise a non-recoverable error and halt the parsing process.
  • evaluation errors, which raise a non-recoverable error and can appear in valid scripts which have invalid constructs (e.g., type mismatches); this kind of errors halt the evaluation process.
  • runtime errors, which are just values and can be handled from the user.

Some builtin functions and methods may return runtime errors, and user-created functions can too. These errors are experimentally considered as normal values, and support for them is work in progress.

Evaluation errors

Evaluation error can be thrown in different scenarios:

  • When calling builtin functions or methods with the wrong number of input parameters or wrong types.
  • When trying to evaluate prefix/infix expressions with invalid types.
  • When trying to index arrays with non integers or when indexing non subscriptable objects.
  • When attempting to call a method on an object that does not have it defined.
  • When attempting to call a non-callable object.

Runtime errors

Runtime errors are errors that can happen while executing code. These can either be:

  • thrown by builtin functions/methods in scenarios in which their signature and types are valid, but the passed values are not (e.g., from_hex("test")).
  • thrown directly by the user through the error builtin function.

These errors do not halt the program execution and are valid values that can be manipulated.

var err = error("test")
print(err, type(err)) // prints "Runtime Error: test on line 1  -  Runtime Error"

The error builtin function

The error builtin function can be used to generate a runtime error value:

var div = fun(x, y) {
  if y == 0 {
    ret error("attempting a division by zero!)
  }
  ret x/y
}

print(f(1, 1)) // prints '1'
print(f(1, 0)) // prints the runtime error

The try keyword

The try keyword can be used before a function/method call, and it changes the way a return value is processed when exiting a block statement.

  • If try is used within a function block, then the function/method call gets evaluated and, in case an error value gets returned, the function exits and that error gets returned.

    var get_header = fun(file, offset, size) {
      // The two following calls may return an error
      // If they do so, the function return that error instead of 
      // continuing in its execution
      var h = try open(file, "hex")
      var contents = try h.read_at(offset, size)
      ret contents
    }
    
    print(get_header(0, 20))
    

    This is syntactic sugar for the following notation:

    var get_header = fun(file, offset, size) {
      var h = open(file, "hex")
      if type(h) == "Runtime Error" {
        ret h
      }
      var contents = h.read_at(offset, size)
      if type(h) == "Runtime Error" {
        ret h
      }
      ret contents
    }
    
    print(get_header(0, 20))
    
  • If try is used in the non-block scope free area, then the function/method call gets evaluated and, in case an error value gets returned, it gets printed to the stderr, and the program exits.

    // If either no argument is passed to the script or a file with 
    // that name does not exist, the program will exit
    var filename = try args[1]
    var e = try open(filename, "elf")
    print(e.sections())
    

The try keyword can be used to intercept user errors too, in the same exact way as shown for errors thrown by builtin functions and methods:

var div = fun(x, y) {
  if y == 0 {
    ret error("attempting a division by zero!)
  }
  ret x/y
}

try div(1, 0)