slashbinbash.de / Muckefuk

Muckefuk (Extension: .mkfk) is a scripting language with a focus on object-based and prototype-based programming. It is very similar to JavaScript, but also borrows ideas from other languages like Java, Python, and Lua.

Download Muckefuk Interpreter (muckefuk-20240630.zip)

To run the interpreter, you will need a Java 17 Runtime Environment.

java -jar muckefuk.jar script.mkfk

Language

Overview

General

The syntax is similar to C-style languages which use ';' to end an expression, and '{' and '}' to define a code block. This makes it more verbose but it gives you freedom to format your code however you like, with a few exceptions.

One of the core concepts of the language is the namespace. In the context of this language, a namespace is an object that holds named references to other objects.

Comments are prefaced by the '#' character and can appear anywhere in the code

# Comment

Assigning objects works as in any other language:

test = 3;

A namespace is defined by using the curly braces '{' and '}':

test = {};

A function is defined by using the func keyword:

fn = func(x, y) {
    return x + y;
};

fn(3, 4);

Data Types

Primitives

NameExample
Booleantrue, false
Integer42
String"string"
Tuple1, "test", true

Tuple

Create a tuple by separating values with a comma ',':

a = 1, true, "test", b

Use tuples to return multiple values from a function:

swap = func(x, y) {
    return y, x;
};

x, y = swap(1, 3);

# x is 3
# y is 1

You can also iterate over tuples:

a = 1, 2, 3, 4;

for (n in a)
    print(n);

# prints 1, 2, 3, 4

Be careful when doing comparisons:

x,y == 1,2      # bad: '==' has precedence over ','
(x,y) == (1,2)  # correct

Enumeration

The enumeration creates distinct named values that can be tested for equality:

Weekdays = enum {
    Monday, Tuesday, Wednesday,
    Thursday, Friday, Saturday,
    Sunday
};

Weekdays.Monday == Weekdays.Monday
    => true

Weekdays.Monday == Weekdays.Friday
    => false

Namespace

To create a namespace use the curly braces '{' and '}':

test = {};

Add an object to a namespace by using the access operator '.':

test = {};
text.x = 3;

You can add objects to a namespace inside the definition:

test = {
    x = 3;
};

You can use other statements inside the definition:

debug = true;
test = {
    if (debug)
        x = 21;
    else
        x = 42;
};

You can use a temporary namespace:

y = {
    a = 10;
    b = 20;
    c = 30;

    x = a + b + c;
}.x;

# y is 60

This works because the namespace is evaluated immediately. All variables that are used in the namespace definition must be already defined.

Function

You can create a function by using the keyword func. When calling a function, you have to pass it the required amount of parameters, otherwise the interpreter will throw an error.

If the function only has one expression or statement, you can use the short version:

fn = func(n) n * n;

fnB(5);  # returns 25

If the function has multiple statements, use '{' and '}' to define which statements belong to the function body.

fn = func(x, y) {
    a = 5;
    b = 10;

    (a * x) + (b * y);
};

fn(2, 3);  # returns 40

The function always returns the result of the last expression that is evaluated.

Use return to exit a function early:

fn = func(n) {
    if (n > 10)
        return 0;
    n;
};

fn(11);  # returns 0
fn(5);   # returns 5

You can assign functions to namespaces:

fnA = func() 12;

test = {};
test.a = fnA;
test.b = func() 42;

test.a();  # returns 12
test.b();  # returns 42

You can pass any object to a function:

fn = func(obj) obj.a;

test = {
    a = 3;
};

fn(test);  # returns 3

Including a function:

fnB = func(x)  x + 10;
fnA = func(fn) fn(10);

fnA(fnB);  # returns 20

Or return a function:

fn = func() {
    func() 42;
};

fn()();  # returns 42

You can also immediately invoke a function after definition:

y = (func(x) { x * x; })(4);

# y is 16

Method

If the first parameter in the function definition is called self, the function will behave like an object method. This means that when the function is called, it will be passed a reference to the object the function is called on, as an additional parameter. This allows you to write obj.fn() instead of having to write obj.fn(obj).

Obj = {
    x = 5;

    fn = func(self, y) self.x * y;
};

objA = {
    x = 8;
};

Obj.fn(2);        # returns 10
Obj.fn(objA, 5);  # returns 40

If you pass an object method the full set of parameters, it will behave like a standalone function.

Closure

You can define a closure by using a namespace, an object method, and the caller concept for syntactic sugar:

createClosure = func(x) {
    o = {};
    o.x = x;
    o.call = func(self, y) self.x + y;
    o;
};

cls = createClosure(10);
cls(5);  # returns 15

Control Structures

If Statement

One-line statements:

i=3;
if (i > 3)
    return 10;
else
    return 20;

Multi-line statements:

i = 3;
if (i > 3) {
    x = 13;
    y = 18;
} else {
    x = 108;
    y = 209;
}

Multiple if clauses:

i = 3;
if (i < 3)
    j = 100;
else if (i == 3)
    j = 200;
else
    j = 0;

If Expressions:

a = if (true) 10 else 20;
b = (if (false) 10 else 20) - a;

fn = func(a,b) a + b;
fn((if (false) 2 else 8), 10); # returns 18

fn = func(c) if(c) 10 else 20;
fn(3 < 8); # returns 10

Be careful to put additional parentheses around the if-expression.

Match Statement

The match statement allows you to do comparisons and destructure tuples with a concise syntax.

You can test a value against one or more values:

match (3) {
    0         => false;
    1         => false;
    2 | 3 | 4 => true;
    _         => false;
}

You can test a value against a range. The end of the range is inclusive.

match (5) {
    0..5 => true;
    _    => false;
}

You can test and destructure a tuple.

Result = enum {
    Ok, Err
};

t = Result.Ok, 16;

match (t) {
    Result.Ok,  n    => print(n);
    Result.Err, msg  => print(msg);
    _                => print("error");
}

# prints 16

The wildcard character '_' matches any value.

The right-hand side of '=>' can be either a statement or a block.

match (n) {
    Result.Ok, a, b => {
        foo(a);
        bar(b);
    }
    _ => print("error");
}

Loop Statement

Repeats the statements in the body of the loop infinitely.

i = 0;
loop {
    i = i + 1;
    if (i == 5)
        break;
}

The curly braces are required. Use the keyword continue to skip to the next iteration. Use the keyword break to jump out of the loop.

You can also specify how many times the body of the loop should be executed:

loop (3) {
    print("Hi!");
}

The number of times must be expressed as a literal integer value.

While Statement

Repeats the statements in the body of the loop while the expression evaluates to true.

i = 0;
while (i < 3) {
    i = i + 1;
}

Use the keyword continue to skip to the next iteration. Use the keyword break to jump out of the loop.

Until Statement

Repeats the statements in the body of the loop until the expression evaluates to true.

i = 0;
until (i == 3) {
    i = i + 1;
}

Use the keyword continue to skip to the next iteration. Use the keyword break to jump out of the loop.

For Statement

The for loop only works with iterable objects:

for (num in range(5))
    print(num);

# prints 0, 1, 2, 3, 4

Iterate over a tuple:

a = 1, 2, 3, 4, 5;

for (i in a)
    print(i);

You can iterate over the elements of an std.Array:

import("std.Array");

arr = Array.new(1, 2, 3, 4);

for (num in arr)
    print(num);

You can iterate over an object that uses the iterator concept.

Use the keyword continue to skip to the next iteration. Use the keyword break to jump out of the loop.

Concepts

Concepts are essentially requirements to an object implementation. If the requirements are met, the object can be used in a special manner.

Constructor

The constructor concept allows you to write Foo{bar} instead of Foo.new(bar), provided that the object has a function called new.

Requires:

new: * -> *

Usage:

Foo = {
    new = func(x,y) {
        o = {};
        o.x = x;
        o.y = y;
        o.z = x + y;
        o;
    };
};

foo = Foo{10, 20};

# foo.x is 10
# foo.y is 20
# foo.z is 30

Caller

The caller concept allows you to write Foo(x,y) instead of Foo.call(x,y), provided that the object has a function called call.

Requires:

call: * -> *

Usage:

Foo = {
    call = func(a, b) a + b;
};

Foo(8,10);  # returns 18

Iterator

The iterator concept allows you to use an object in a for loop, provided that the functions hasNext and next are implemented.

Requires:

hasNext: self -> bool
next: self -> *

Usage:

iter = {
    i = 0;

    hasNext = func(self) self.i < 10;

    next = func(self) self.i = self.i + 1;
};

for (n in iter)
    print(n);

Scope

Similar to Python, each module has different namespaces. A built-in namespace, a global namespace, and function namespaces.

The global namespace of a module is like the root namespace of the document:

x = 3;
fn = func() x;  # x is found in the global namespace

fn();          # returns 3

The function namespace only exists temporarily, during the function call:

x = 3;
fn = func() {
    x = 42;
    global.x = 21;
    x;
};

fn();   # returns 42
# x is 21

Use the name global to access the global namespace.

Other access patterns:

test = {
    age = 3;
    maxAge = age + 5;       # OK name on same level

    fnA = func() age;       # ERROR there is no 'age' in function namespace
                            # or global namespace

    fnB = func() test.age;  # OK access through global namespace

    fnC = func(o) o.age;    # OK access through object passed as parameter
};

Scope affects the lifetime of objects. If you create objects in the global scope, they live as long as the module lives. If you create objects in the function scope, they live during function execution. The garbage collector takes care of freeing the objects that are not referenced anymore.

Modules

Each Muckefuk file is a module. The name of the module is the file name. The address of the module is its directory. For instance, if you have a file named mkfk/Foo.mkfk, the addresses of the module is mkfk.Foo.

You can import a module by using the import function:

import("mkfk.Foo");

Foo.bar();

The function imports Foo and all its names into the global namespace of the module.

You can import names directly into the namespace by using the import_static function. You can explicitly select which name you want to import, or use an asterisk '*' to import all names:

import_static("mkfk.Foo.*");

bar();

You can import modules into namespaces and function scopes:

MyFoo = {
    import_static("mkfk.Foo.*");
};

MyFoo.bar();

When you are creating a module, all names are private by default. You have to explicitly export the names that other modules should see:

export("name", "bar");

number = 8;
name = "testing";

bar = func() number + 10;

Only name and bar are visible to modules that import this module.

Prototype-based Programming

The language doesn't have classes. Instead, you can create prototypes and instances of these prototypes.

Animal = {
    age = 0;

    new = func(age) {
        c = copy(Animal);
        c.age = age;
        c;
    };

    decAge = func(self, value) {
        self.age = self.age - value;
    };
};

a = Animal.new(42);
a.decAge(2);    # age is now 40

b = Animal.new(24);
b.decAge(4);    # age is now 20

You have to construct the instance of the Animal class yourself, by using the copy function on the prototype, modifying the copy, and returning it.

With this concept in mind, you can implement inheritance, polymorphism, or mixins. You can extend the functionality of an instance by adding new functions on-the-fly, or build an instance based on functions from different namespaces.

Animal = {
    age = 0;

    new = func(age) {
        c = copy(Animal);
        c.age = age;
        c;
    };

    getAge = func(self) self.age;
};

Cat = {
    new = func(age) {
        c = Animal.new(age);
        c.getAge = Cat.getAge; # overwrite function
        c;
    };

    getAge = func(self) self.age * 7; # cat years
};

Dog = {
    new = func(age) {
        c = Animal.new(age);
        c.getAge = Dog.getAge; # overwrite function
        c;
    };

    getAge = func(self) self.age * 10; # dog years
};

extend = func(obj) {
    obj.legs = 4;
    obj.canWalk = func(self) self.legs > 0;
    obj;
};

A = extend(Cat.new(10));
B = extend(Dog.new(10));

As you can see, you are free to do whatever you like. The only disadvantage is that it is quite verbose and there is no security of any kind.

Built-in Functions

Range

Create an iterator that iterates over a certain range:

for (i in range(5))
    print(i);

Type

Get the type of an object:

type(5);
    => "int"
type("Foo");
    => "str"

Can be used in functions to handle different types of inputs.

Use

Import names from an object into a namespace:

obj = {
    A = 5;
    B = 18;
};

use(obj);  # import all names of obj into global namespace

print(A, B);
    => 5 16

This can also be done inside other namespaces or functions.

Java Integration

The Java integration is very rudimentary at this point. It has a few restrictions:

Java

package de.slashbinbash.muckefuk;

public class TestClass {
    public int value;

    public static int testA(int a, int b) {
        return a + b;
    }

    public TestClass(int value) {
        this.value = value;
    }

    public void printValue() {
        System.out.println(value);
    }
}

Muckefuk

import("de.slashbinbash.muckefuk.TestClass");

# static method call
TestClass.testA(5, 10);  # returns 15

# constructor call
obj = TestClass.new(42);
obj.printValue();        # prints 42