-
Notifications
You must be signed in to change notification settings - Fork 0
Language Constructs
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
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"
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)
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 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 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 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 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)