Skip to content

ashn-dot-dev/mellifera

Repository files navigation

The Mellifera Programming Language

Mellifera is a scripting language featuring mutable value semantics with explicit references, structural equality, copy-on-write data sharing, strong dynamic typing, and a lightweight nominal type system with structural protocols.

println("hello 🐝");
$ mf examples/hello.mf
hello 🐝

Mellifera aims to be an enjoyable language for writing small scripts and CLI utilities. Here is an example word counting program in Mellifera showcasing some of the syntax and semantics of the language.

#!/usr/bin/env mf
# usage: cat FILE | word-count-simple.mf
let words = re::split(input(), r`\s+`)
    .map(function(word) {
        return re::replace(word.to_lower(), r`[^\w']`, "");
    })
    .filter(function(word) {
        return word.count() != 0;
    });

let counts = Map{};
for word in words {
    try { counts[word] = counts[word] + 1; }
    catch { counts[word] = 1; }
}

let ordered = counts
    .pairs()
    .sorted_by(function(lhs, rhs) {
        return rhs.value - lhs.value;
    });
for pair in ordered {
    println($"{pair.key} {pair.value}");
}
$ cat /tmp/peter-piper.txt
Peter Piper picked a peck of pickled peppers,
A peck of pickled peppers Peter Piper picked;
If Peter Piper picked a peck of pickled peppers,
Where's the peck of pickled peppers Peter Piper picked?

$ cat /tmp/peter-piper.txt | mf examples/word-count-simple.mf
peter 4
piper 4
picked 4
peck 4
of 4
pickled 4
peppers 4
a 3
if 1
where's 1
the 1

Mellifera features value semantics, meaning assignment operations copy the contents of a value when executed. After an assignment statement such as a = b, a and b will contain separate copies of the same value. Mellifera also performs equality comparisons based on structural equality, so two values are considered to be equal if they have the same contents.

let x = ["foo", {"bar": 123}, "baz"];
let y = x; # x is assigned to y by copy
println($`x is {x}`);
println($`y is {y}`);
# x and y are separate values that are structurally equal
println($`x == y is {x == y}`);

print("\n");

# updates to x and y do not affect each other, because they are separate values
x[0] = "abc";
y[1]["bar"] = "xyz";
println($`x is {x}`);
println($`y is {y}`);
# x and y are no longer structurally equal as their contents now differ
println($`x == y is {x == y}`);

print("\n");

let z = ["foo", {"bar": "xyz"}, "baz"];
println($`z is {z}`);
# y and z are separate values that are structurally equal
println($`y == z is {y == z}`);
$ mf examples/value-semantics-and-structural-equality.mf
x is ["foo", {"bar": 123}, "baz"]
y is ["foo", {"bar": 123}, "baz"]
x == y is true

x is ["abc", {"bar": 123}, "baz"]
y is ["foo", {"bar": "xyz"}, "baz"]
x == y is false

z is ["foo", {"bar": "xyz"}, "baz"]
y == z is true

Unlike most scripting languages, in which reference semantics are the default way of assigning and passing around composite data, Mellifera uses mutable value semantics with explicit references. One may obtain a reference to a value using the postfix .& operator, then later dereference that reference-value to get the original referenced value using the postfix .* operator.

let pass_by_value = function(val) {
    println($"[inside pass_by_value] val starts as {val}");

    # Here val is a copy, so the mutation performed by `val.push` will not
    # change the original value outside of this function's lexical scope.
    val.push(123);

    println($"[inside pass_by_value] val ends as {val}");
};

let pass_by_reference = function(ref) {
    println($"[inside pass_by_reference] ref starts as {ref} with referenced value {ref.*}");

    # Here ref is a reference to the original value outside of this function's
    # lexical scope, so mutation performed by `ref.*.push` will change that
    # original value.
    ref.*.push(123); # .* is the postfix dereference operator

    println($"[inside pass_by_reference] ref ends as {ref} with referenced value {ref.*}");
};

let x = ["foo", "bar", "baz"];
println($"[outside pass_by_value] x starts as {x}");
pass_by_value(x);
println($"[outside pass_by_value] x ends as {x}");

print("\n");

let y = ["abc", "def", "ghi"];
println($"[outside pass_by_reference] y starts as {y}");
pass_by_reference(y.&); # .& is the postfix reference operator
println($"[outside pass_by_reference] y ends as {y}");
$ mf examples/explicit-references.mf
[outside pass_by_value] x starts as ["foo", "bar", "baz"]
[inside pass_by_value] val starts as ["foo", "bar", "baz"]
[inside pass_by_value] val ends as ["foo", "bar", "baz", 123]
[outside pass_by_value] x ends as ["foo", "bar", "baz"]

[outside pass_by_reference] y starts as ["abc", "def", "ghi"]
[inside pass_by_reference] ref starts as reference@0x372ec01e3390 with referenced value ["abc", "def", "ghi"]
[inside pass_by_reference] ref ends as reference@0x372ec01e3390 with referenced value ["abc", "def", "ghi", 123]
[outside pass_by_reference] y ends as ["abc", "def", "ghi", 123]

Mellifera's mutable value semantics mean that references can only be created at specific points in a program: using the .& operator on a function argument, implicitly for self receiver arguments in methods declared with function.&(self, ...), or when declaring loop variables with a postfix .&.

# Reference created by using the reference operator .& on a function argument.
let f = function(x) {
    println($`x is {x} of type {typename(x)}`);
    println($`x.* is {x.*} of type {typename(x.*)}`);
};
let v = ["foo", "bar", "baz"];
f(v.&);

print("\n");

# Reference created referring to each vector element in a for loop.
let inventory = [
    {"item": "apples", "quantity": 12},
    {"item": "bananas", "quantity": 30},
    {"item": "cherries", "quantity": 8},
];
println($`inventory starts as {inventory}`);
for stock.& in inventory {
    println($`Doubling the quantity of {stock.*.item}`);
    stock.*.quantity = stock.*.quantity * 2;
}
println($`inventory ends as {inventory}`);

print("\n");

# Reference created referring to each map value in a for loop.
let bank_accounts = {
    "Ashley": {"checking": 350, "savings": 1500},
    "Joseph": {"checking": 120, "savings": 45},
};
println($`bank_accounts starts as {bank_accounts}`);
for person, account.& in bank_accounts {
    println($`Adding $100 to {person}'s savings account`);
    account.*.savings = account.*.savings + 100;
}
println($`bank_accounts ends as {bank_accounts}`);

print("\n");

# Reference implicitly created and passed as the first argument when a method
# declared with function.& is invoked with the dot operator.
let person = type {
    .init = function(name, age) {
        return new person {.name = name, .age = age};
    },
    .birthday = function.&(self) {
        println($`Happy birthday {self.name}! 🥳`);
        self.*.age = self.*.age + 1;
    },
};
let ashley = person::init("Ashley", 21);
println($`ashley starts as {ashley}`);
ashley.birthday(); # ashley.& is implicitly passed as self to person::birthday()
println($`ashley ends as {ashley}`);
$ mf examples/creating-references.mf
x is reference@0x4d9c24eb58a0 of type reference
x.* is ["foo", "bar", "baz"] of type vector

inventory starts as [{"item": "apples", "quantity": 12}, {"item": "bananas", "quantity": 30}, {"item": "cherries", "quantity": 8}]
Doubling the quantity of apples
Doubling the quantity of bananas
Doubling the quantity of cherries
inventory ends as [{"item": "apples", "quantity": 24}, {"item": "bananas", "quantity": 60}, {"item": "cherries", "quantity": 16}]

bank_accounts starts as {"Ashley": {"checking": 350, "savings": 1500}, "Joseph": {"checking": 120, "savings": 45}}
Adding $100 to Ashley's savings account
Adding $100 to Joseph's savings account
bank_accounts ends as {"Ashley": {"checking": 350, "savings": 1600}, "Joseph": {"checking": 120, "savings": 145}}

ashley starts as {"name": "Ashley", "age": 21}
Happy birthday Ashley! 🥳
ashley ends as {"name": "Ashley", "age": 22}

References cannot be assigned to a new variable, stored in a container, closed over by a function, or propagated up the call stack with return or error statements. Because of this, the language guarantees that a reference can never outlive the scope in which it was created, without requiring escape analysis.

$ mf -c 'let x = 123; let f = function(ref) { let y = ref; }; f(x.&);'
[<command>, line 1] error: attempted assignment statement with type reference to number
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { let y = [ref]; }; f(x.&);'
[<command>, line 1] error: invalid vector construction with element reference@0x24739de8d410
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { let y = {"foo": ref}; }; f(x.&);'
[<command>, line 1] error: invalid map construction with key "foo" and value reference@0x193422f0d410
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { let y = {ref}; }; f(x.&);'
[<command>, line 1] error: invalid set construction with element reference@0x7173f42db410
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { function() {}; }; f(x.&);'
[<command>, line 1] error: {"message": "function closes over a reference", "references": [{"name": "ref", "location": {"file": "<command>", "line": 1}}]}
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { return ref; }; f(x.&);'
[<command>, line 1] error: attempted return statement with type reference to number
...within f@[<command>, line 1] called from <command>, line 1

$ mf -c 'let x = 123; let f = function(ref) { error ref; }; f(x.&);'
[<command>, line 1] error: attempted error statement with type reference to number
...within f@[<command>, line 1] called from <command>, line 1

Mellifera contains a small number of core built-in types with first-class language support: null, boolean, number, string, regular expression, vector, map, set, and function, as well as a second-class reference type. These built-in types are sufficient for most simple programs, but when the need arises for more structured data, users have the option to define custom types with specific behavior.

let vec2 = type {
    .fixed = function(self, ndigits) {
        return new vec2 {
            .x = self.x.fixed(ndigits),
            .y = self.y.fixed(ndigits),
        };
    },
    .magnitude = function(self) {
        return math::sqrt(self.x * self.x + self.y * self.y);
    },
};

let va = new vec2 {.x = 3, .y = 4};
println($`va is {va} with type {typename(va)}`);
println($`va.magnitude() is {va.magnitude()}`);

print("\n");

let vb = new vec2 {.x = math::e, .y = -math::pi};
println($`vb is {vb} with type {typename(vb)}`);
println($`vb.magnitude() is {vb.magnitude()}`);
println($`vb.fixed(3) is {vb.fixed(3)}`);
$ mf examples/user-defined-types.mf
va is {"x": 3, "y": 4} with type vec2
va.magnitude() is 5

vb is {"x": 2.718281828459045, "y": -3.141592653589793} with type vec2
vb.magnitude() is 4.154354402313313
vb.fixed(3) is {"x": 2.718, "y": -3.142}
let fizzbuzzer = type extends iterator {
    .init = function(n, max) {
        return new fizzbuzzer {"n": n, "max": max};
    },
    .next = function.&(self) {
        let n = self.n;
        if n > self.max {
            error null; # error null signals end-of-iteration
        }
        self.n = self.n + 1;
        if n % 3 == 0 and n % 5 == 0 {
            return "fizzbuzz";
        }
        if n % 3 == 0 {
            return "fizz";
        }
        if n % 5 == 0 {
            return "buzz";
        }
        return n;
    },
};

let fb = fizzbuzzer::init(1, 15);
for x in fb {
    println(x);
}
$ mf examples/user-defined-iterators.mf
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz

Mellifera is intended to be a practical language with reasonable exception-based error handling and pleasant top-level error traces for when things go wrong.

try {
    fs::read("/path/to/file/that/does/not/exist.txt");
}
catch err {
    eprintln($"error: {err}");
}

eprint("\n");

let g = function() {
    let f = function() {
        error "oopsie";
    };
    f();
};
function() {
    let h = function() {
        g();
    };
    h();
}();
$ mf examples/exceptions.mf
error: failed to read file "/path/to/file/that/does/not/exist.txt" (file not found)

[examples/exceptions.mf, line 12] error: oopsie
...within f@[examples/exceptions.mf, line 11] called from examples/exceptions.mf, line 14
...within g@[examples/exceptions.mf, line 10] called from examples/exceptions.mf, line 18
...within h@[examples/exceptions.mf, line 17] called from examples/exceptions.mf, line 20
...within function@[examples/exceptions.mf, line 16] called from examples/exceptions.mf, line 21

A brief language overview can be found in overview.mf, and additional example programs can be found under the examples directory.

Installing

Build the Mellifera interpreter + associated tooling and install Mellifera to the location specified by MELLIFERA_HOME (default $HOME/.mellifera):

make build install                               # Install to $HOME/.mellifera
make build install MELLIFERA_HOME=/opt/mellifera # Install to /opt/mellifera

Then, add the following snippet to your .bashrc (or equivalent shell configuration file), replacing $HOME/.mellifera with your chosen MELLIFERA_HOME directory if installing to a non-default MELLIFERA_HOME location:

export MELLIFERA_HOME="$HOME/.mellifera"
export PATH="$MELLIFERA_HOME/bin:$PATH"

Finally, open a new interactive shell and verify the Mellifera interpreter was successfully installed with:

$ printf 'println("Hello world!");' | mf /dev/stdin
Hello world!

Development

Mellifera currently has two implementations: a library & interpreter written in Go (mellifera.go and cmd/mf/mf.go), which serves as the primary language implementation, and an older reference interpreter written in Python (mf.py), which served as the original language implementation during prototyping. In order to ensure that Mellifera does not depend on a particular host language, both implementations are updated to support the same core set of language features and built-in functions. Most users will want to use the Go implementation, and most make targets will default to the Go version of that target. However, specific *-go and *-py versions of targets should be used when hacking on either the Go implementation or Python implementation, respectively.

General Development (Default Go Implementation)

make build   # build standalone interpreter executable
make check   # run unit tests and interpreter golden tests
make format  # format sources
make install # install standalone Mellifera tooling

Development on the Go Library & Interpreter

make build-go  # build standalone interpreter executable
make wasm-go   # build Wasm module for embedding in the browser
make check-go  # run unit tests and interpreter golden tests
make format-go # format sources using go fmt
make install   # install standalone Mellifera tooling

The Go Mellifera implementation has experimental support for running Mellifera programs in the browser via WebAssembly. In a terminal, run the following command:

make wasm-go && python3 -m http.server

Then, navigate to http://localhost:8000/mellifera.html in your web browser to interact with a minimal web build of the interpreter.

Development on the Python Reference Interpreter

python3 -m venv .venv-mellifera
. .venv-mellifera/bin/activate
python3 -m pip install -r requirements.txt

make check-py  # run interpreter golden tests
make lint-py   # lint with mypy and flake8
make format-py # format using black

Development on Both Interpreters

There is a script, tools/validate-compatibility.sh, that will validate that the token stream and abstract syntax tree representations produced by both interpreters are identical for a large set of programs in the Mellifera repository, including all example and test programs:

sh tools/validate-compatibility.sh

To build, format, lint, run tests, and validate compatibility between both implementations, use the following one-liner:

make clean format-go format-py lint-py build-go wasm-go check-go check-py && sh tools/validate-compatibility.sh

Contributing

Contributions are welcomed and appreciated! 🐝

One of the best ways to help Mellifera is to use the language for your own projects and let me know what works and what needs improvement. Is Mellifera missing a critical built-in function? Is there a misleading error message that could use some attention? Did you find a bug in the interpreter? Let me know by creating a new issue!

Want to assist the project through code or documentation contributions? Take a look at open issues marked with the help-wanted label for areas where your help would be particularly appreciated.

We aim for every commit in the Mellifera repository to be complete and self-contained. Before submitting a pull request, please rebase and squash your commits into logical units of change. For any non-trivial update, your commit messages should contain information about what the commit does as well as why the commit was needed. Examples of good commit messages can be found here and here.

Strict No LLM / No AI Policy

Mellifera is a language developed by humans, for humans. 🐝❤️

Use of LLMs for issues, comments, and pull requests is not permitted.

License and Credits

All content in this repository, unless otherwise noted, is licensed under the Zero-Clause BSD license.

See LICENSE for more information.

The Mellifera logo and Melli character were designed by Natalie Jara.

About

A scripting language with mutable value semantics

Topics

Resources

License

Stars

4 stars

Watchers

0 watching

Forks

Contributors