Skip to content

Language features

HEMMOUDA Aymane edited this page Feb 2, 2024 · 26 revisions

As stated before, the language by it self is very basic, and it pretty much only has a couple of features that facilitate manipulating numbers (which is the only available data type..) with the given 6 operations.

So what are these features and mechanics that the languages has?

Scopes and variables

Scopes in the language are created when a function is called, or by defining one like so:

# Main scope

{ # New scope starts here
    # Anything can go here
} # And it ends here

# And back to the main scope

Each scope has its own variables, and you can reference variables from the parent scopes, as long as they are not shadowed by a variable that the scope it self has. This means that variable references are resolved recursively, i.e. it first checks itself, then its parent's scope then that parent scope's parent scope and so on..

For example:

foo = 5

{
    bar = foo + 2 # This foo references the variable from the main scope
    foo = 10 # Now this is a new variable called foo in this scope. And the foo from the main scope is shadowed
    bar = bar - foo # This foo references to the variable we just created. And not foo from the parent scope
}

ret bar # ERROR: we can't return bar, because bar is unknown here.

Variables can have the same name as already existing functions. No problem with that.

UPPER_CASE variables should be treated as constants. And just like in Python, you can change them, but don't.

Another thing (not directly related to variables or scopes, but oh well): each instruction spans one line, but you can define multiple instructions in one line by terminating each on of them with ;. In similar fashion to JavaScript.

Special variables

Special variable: res

Since there cannot be anything like null or None in the language, each function call or scope must return a number (user defined scopes also return a value, and so they can be used to assign a value to a variable). To ensure that, a special variable called res (stands for rizz result) is implicitly created and initialized to 0 at the start of each new scope (a new call to a function is a new scope of course). And if no value is returned from the scope by the end, either by using the ret (return) keyword to return a value or a variable, or if it's used but the value to be returned is omitted, the res variable is returned instead.

Special variable: ext_res

Another special variable that also implicitly exists is ext_res (stands for extra rizz external result). This special variable however exist in all scopes but the main one. This variable references the parent scope's res variable.

That is because the parent scope's res variable is always shadowed by the scope's own res variable, and so without ext_res, there is no way of reference it.

Be careful tho, if you create a variable called ext_res it will shadow it.

Small example:

res = 5 # I assign the value 5 to the already existing res variable of the main scope

res = { # I then re-assign the result of this scope to it.

    # some code ...

    ret ext_res + 10 # At the end I return what ever the parent res is + 10. In this case it's 5 + 10.
}

Keyword: ext

The ext keyword (stands for external) can, only, be used to assign values to variables from the parents scopes.

For example:

foo = 5

{
    ext foo = 10 # The variable foo of the main scope now has 10
}

ret foo # foo == 10

It can also be used with res to modify the return value of the parent scope. But not with ext_res.

{
    ext res = 3.14
}

# The program will return 3.14

Functions

Basics

Functions are defined by using the def keyword like so:

def func_name (param_1, param_2, param_3) {
    # Any code..
    # You can even define functions inside of functions.
}

and they can be called like so:

res = func_name(10, res, 20)

Functions overloading is supported too:

def func () {
    ret -5
}

def func (param) {
    ret param + 2
}

ret func(10) # This will know to call the second function. And the result of the compiled operation will be 12

Aliases

A quality of life feature with function is that you can define aliases to call functions with instead of their name. However, only functions with 1 parameter or 2 parameters can have aliases. For example:

$this_is_a_unary_alias
def function_with_1_param (param) {
    # some code ...
}

@and_this_is_a_binary_alias
def function_with_2_params (param_1, param_2) {
    # some code ...
}

We use $ for unary aliases (for functions with 1 parameter) and @ for binary aliases (for functions with 2 parameters). An alias can contain special characters, keywords even, and pretty much anything that you want, they are only terminated by a space.

These aliases are useful to make the code more readable, for example:

TRUE = 1
FALSE = 0

$!
def not (bool) {
    # Inverts a boolean
    ret (bool - 1) / -1
}

ret $! FALSE

In this example, the unary alias $! will call the function not with the value on its right. And that is the case for all unary aliases, they call their function with whatever immediate value to their right.

As for binary aliases (@), they calls their function with whatever is on their left as the first parameter, and the value immediately to their right as the second parameter, for example:

@+
def special_add (a, b) {
    # A special add example
    ret a + b - 5
}

ret 1 @+ 2 @+ 3

The above is the same thing as:

ret special_add(special_add(1, 2), 3)

The aliases don't shadow the functions' actual name and you can still call functions with their name, even if they have aliases.

Aliases however can only be used with values, and cannot be used to simply call functions;

$1
def foo (param) {}

foo(res) # This is okay

$1 res   # This will not work

Main function

If a function called main is encountered in the main scope, it gets unwrapped (its body gets put in the main scope) and thus executed immediately and only once, and is never defined (meaning you can't call it).

If the main function defines parameters, those parameters are expected to be provided by the user in the command line when running the file.

Recursion

One last teeny tiny, very small, not important, negligible, not useful detail about functions, is that there is no recursion.. Functions are defined only after their body is terminated (after they hit the }), and so, you can't call a function before it's defined, which means no recursion.

But why not, tho?

I talk about that here. But basically, I want it to be so that numbers and operations do everything and not the compiler.

Chars

Thought you said only numbers?

Well yes, only numbers. Any char that is written is just syntactic sugaring for its ASCII value.

For example:

ret 'a'

is the exact same as

ret 97 # ASCII code for the char `a`

Strings

I go into more details about strings here, but basically they are also just syntactic sugaring for numbers.

You can define strings using ", like so:

str = "This is a \"string\". lol"

Keyword: include

The include keyword allows you to include other files inside your main file. However, it is just a glorified copy and paste! When you include a file, it is as if you took that file and pasted it right where you included it. This means that there is no file scope or something.

The syntax to include is like so:

include file_1, file_2.mlg, file_3 # The file extension is automatically added if the file was not found

When trying to include a file, the runner tries to locate that file, first relative to where the main file is, if it failed, it looks for it in the lib dir.

The file extension is .mlg but it's not required that you add it when including a file, as that it will try to look for both; the file you provided, and a version of it that has .mlg at the end if it doesn't have it.

Also, the same file is only included once.

For loops

And finally, for loops, but not just any for loops, only deterministic finite for loops. You specify the starting index (included obviously), end index (also included), the step, and the variable that will hold the iteration step.

for (i: 1: 10: 1) {
    # any code..
    # This loop will go from i == 1, all the way to i == 10, with a step of 1.
}

If you only want to iterate for a certain amount of times you can just specify the end index as such:

for (100) {
    # something a 100 times
}

and the starting index as well as the step will default to 1.

You can also only specify the end index, and the variable like so:

for (i: 10) {
    res = res + i
}

Or only the starting index, the end index and the variable, and then the step will default to 1:

# The above example is the same as
for (i: 1: 10) {
    res = res + i
}

Lastly, if the step is negative, the iteration order is flipped, and it starts from the end index and keeps decrementing all the way to the starting index.

Know that these for loops don't create scopes, or don't repeatedly "execute code" they just get unwrapped and duplicated how many ever times the indexes specify. And so, for example this:

for (i: 3) {
    res = res + i
}

will get unwrapped to become exactly this:

i = 1
res = res + i
i = 2
res = res + i
i = 3
res = res + i

Important detail about for loops

Allowing just for constant for loops (ones that don't use variables in their indexes) isn't as practical and very limiting, and so for loops allow the use of variables as indexes. However that introduces a foreign mechanic to the language, which is that whether something happens or not, is determined by the compiler, and not just the math.

This allows for the existence of real practical if statements. For example:

include std

condition = FALSE

# if
for (_: $! condition: 0) {
    # If the condition is TRUE, this will get unwrapped once
    ret 10
}

# else
for (_: condition: 0) {
    # If the condition is FALSE, this will also get unwrapped once
    ret 20
}

# The program will return 20

and I feel that that takes a bit from the language. However, this mechanic is never taken advantage of in any of the libraries, and only the normal use of for loops ever takes place *.

*

I do in fact use this mechanic in a couple of places, but only for speed purposes (instead of executing the whole thing, only execute some part of it), and only if I came up with a version of it that produces the same exact result without using it. But it's generally slower, so I use the other one.

Notes to keep in mind

If you want to play around with the language, keep the following in mind:

  • There is no actual boolean type, only numbers, and they are as such:
    • FALSE is 0
    • TRUE is 1
    • Anything else is "invalid"
  • Negative numbers in the language exists as they're, so -5^2 == 25 but - 5^2 == -25. And so, this code for example is invalid ret 1 -1, but this one isn't ret 1 - 1.
  • Most (all but two, I think) functions don't check for the coherency of the parameters, and will only give you valid results if the parameters are coherent. For example, you can't give the if function 2 in condition and expect that it'll work and consider it as TRUE because it's different than 0. Nor do strings functions expect the numbers to be negative..
  • Keep in mind that, there is no real if statement, and that what ever you put in the else part of the if function, also get's "executed" / computed. It still is just a function after all, both arguments get evaluated.
  • If you venture trough the libraries' code, you'll encounter functions that have the prefix NN, which stands for Non-Negative. These functions expect to only work with positive or null numbers. However, these functions only exist inside other functions, and so they are not visible outside.

Goto previous page.

Goto next page.