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
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);
Name | Example |
---|---|
Boolean | true , false |
Integer | 42 |
String | "string" |
Tuple | 1, "test", true |
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
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
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.
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
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.
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
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.
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");
}
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.
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.
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.
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 are essentially requirements to an object implementation. If the requirements are met, the object can be used in a special manner.
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
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
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);
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.
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.
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.
Create an iterator that iterates over a certain range:
for (i in range(5))
print(i);
Get the type of an object:
type(5);
=> "int"
type("Foo");
=> "str"
Can be used in functions to handle different types of inputs.
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.
The Java integration is very rudimentary at this point. It has a few restrictions:
int
, boolean
, String
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