Muse's Design Goals

Muse is a dynamic language designed to allow Rust developers a lightweight way to run untrusted scripts. It is designed to be familiar to users of other dynamic programming languages, while also feeling inspired by Rust.

Muse aims to achieve these overarching goals with the goals and themes described in the remainder of this chapter.

Executing Untrusted Code Safely

One of the most famous unsolved computer science problem is P vs NP, sometimes referred to the halting problem. To summarize, currently we believe it is impossible to determine if a piece of code terminates if the code is written for a Turing-complete language. Most programming languages, including Muse, are Turing-complete.

Muse provides three approaches that can be combined for allowing applications to run untrusted code while not allowing Muse to consume more resources than allowed.

  • Virtual Machine Isolation + Limits: Each virtual machine is fully isolated and has its own configurable limitations. It should be impossible to cause a virtual machine to panic: in other words, all panics should be considered bugs in Muse. Panic-free execution is a work in progress, and additional limits are needed to achieve this goal.
  • Budgeted Execution: Each virtual machine can be allocated a budget. Each instruction the virtual machine executes will consume some of the budget. When no budget is available, the virtual machine is paused and can be resumed once additional budget is provided.
  • Timed Execution: The virtual machine has separate functions for executing and resuming with specific limits on execution time. Budgeted execution on its own is not enough if the application exposes any async functionality to Muse, as an async task may block for an indefinite amount of time without incurring any budget charges.

Bring your own standard library

Out of the box, a Muse virtual machine only supports the types needed to implement the base language:

  • Nil
  • Booleans
  • 64-bit unsigned integers
  • 64-bit signed integers
  • 64-bit floating point numbers
  • Symbols
  • Dynamic Types
    • Strings
    • Regular Expressions
    • Lists/Tuples
    • Maps/Sets
    • Functions
    • Modules

The application embedding Muse has precise control over what to expose beyond this basic set of functionality.

The Dynamic Types are types that Muse provides that are implemented using the CustomType trait in Rust. By implementing CustomType for your own types, you can use Value::dynamic() to expose your types to Muse.

Additionally, Muse provides two types for exposing functions to Muse:

  • RustFunction: A regular Rust function that returns a Result<Value, Fault>
  • AsyncRustFunction: A function that returns a type that implements Future<Output = Result<Value, Fault>>

Consistent, Minimal Design

Muse aims to have a minimal syntax while still being very powerful. It does this by trying to reuse syntax as much as possible to ensure consistency across all aspects of the language.

The most prevalent example is how pattern matching syntax is not only used in match but also in for, let, var, and fn.

Magical, Yet Predictable

Muse embraces duck typing: if it walks like a duck and it quacks like a duck, then it must be a duck. To support various language features, the compiler generates code that tries to use a value in a specific way. For example, the code generated by these two expressions is identical:

let a = [0, 1, 2];

# Access the first element using indexing.
a[0];
# Access the first element using the `get` function.
a.get(0);

When the compiler generates code for the index operator, it generates code that invokes get on the target value with the given parameters. When the compiler assigns a value through the index operator, the compiler invokes set on the target value with the assigned value in the final argument location.

The compiler takes a similar approach when implementing pattern match destructuring as well as for loop iteration. If a value responds to the correct functions, it can be used within those language features.

Let it crash

Muse embraces exception handling with a goal of making it straightforward and concise to add exception handling. This may seem like an odd choice when it's designed to be used by Rust developers, but in a way, Rust also embraces exception handling: panics. Consider what Rust and Muse do when dividing an integer by 0. In Rust, it unwinds the stack to the panic handler. In Muse, it unwinds the stack to the exception handler.

In Rust, it's much easier to check for 0 compared to catching an panic. In Muse, the try operator (?) makes it easy to turn any exception into nil:

# '//' is the integer division operator
(1 // 0)?

Rather than having two types of error handling, Muse attempts to unify all error handling through exceptions and pattern matching.

Future of Muse

  • Tagged Enumerations
  • Structures
  • (Optional) Actor-based runtime similar to BEAM

Distant Future of Muse

We want to bring gradual typing to Muse, where functions are written using a strongly-typed variant of Muse. We have reserved the slim arrow (->) operator for specifying the return type of functions and closures, and the grammar supports adding ':' <Type> rules in the necessary location(s).

Optional JIT compilation paired with strongly-typed functions could yield fairly efficient code.

These long-term goals are unlikely to see any progress, as the initial language features are still being implemented.

Language Reference

Grammar Syntax

To specify the allowed grammar rules, this syntax is used:

  • RuleName: [..];: A named grammar rule that matches the specified terms.
  • <RuleName>: A reference to a rule named RuleName
  • 'text': The raw characters text
  • ( x ): A grouping of grammar terms
  • x | y: Either x or y, with x having precedence.
  • <x | y>: Either rule x or rule y with equal precedence.
  • x?: Optionally allow x
  • x*: Zero or more x in a row.
  • x+: One or more x in a row.
Program: <Chain>;

Chain: <Expression> (';' <Expression>)*;

Expression: <Assignment> | <InlineIf>;

Assignment: <Lookup | Index> ('=' <Assignment>)*;

InlineIf: <LogicalOr> ('if' <LogicalOr> ('else' <Expression>)?)?;

LogicalOr: <LogicalXor> ('or' <LogicalXor>)*;
LogicalXor: <LogicalAnd> ('xor' <LogicalAnd>)*;
LogicalAnd: <Comparison> ('and' <Comparison>)*;

Comparison:
    <BitwiseOr>
    <LessThanOrEqual |
        LessThen |
        Equal |
        NotEqual |
        GreaterThan |
        GreaterThanOrEqual>*;
LessThanOrEqual: '<=' <BitwiseOr>;
LessThan: '<' <BitwiseOr>;
Equal: '=' <BitwiseOr>`;
NotEqual: '!=' <BitwiseOr>;
GreaterThan: '>' <BitwiseOr>;
GreaterThanOrEqual: '>=' <BitwiseOr>;

BitwiseOr: <BitwiseXor> ('|' <BitwiseXor>)*;
BitwiseXor: <BitwiseAnd> ('^' <BitwiseAnd>)*;
BitwiseAnd: <AddSubtract> ('&' <AddSubtract>)*;

AddSubtract: <MultiplyDivide> <Addition | Subtraction>*;
Addition: '+' <MultiplyDivide>;
Subtraction: '-' <MultiplyDivide>;

MultiplyDivide:
    <Power>
    <Multiply | Divide | Remainder | IntegerDivide>*;
Multiply: '*' <Power>;
Divide: '/' <Power>;
Remainder: '%' <Power>;
IntegerDivide: '//' <Power>;

Power: <Punctuation> ('**' <Punctuation>)*;

Punctuation: <Prefix> <Call | Index | Lookup | TryOperator | NilCoalesce>*
Call: '(' <ExpressionList> ')' <Punctuation>?;
Index: '[' <ExpressionList> ']' <Punctuation>?;
Lookup: '.' <Identifier> <Punctuation>?;
TryOperator: '?' <Punctuation>?;
NilCoalesce: '??' <Punctuation>;

ExpressionList: <Expression> (',' <Expression>)* ','?;

Prefix:
    <BlockOrMap |
        Tuple |
        List |
        LogicalNot |
        BitwiseNot |
        Negate |
        Mod |
        Pub |
        Fn |
        Let |
        Var |
        If |
        Literal |
        Loop |
        While |
        For |
        Labeled |
        Continue |
        Break |
        Return |
        Match |
        Try |
        Throw> | Term;

Literal: 'true' | 'false' | 'nil';

BlockOrMap: '{' <EmptyMap | BlockBody | MapBody | SetBody> '}';
EmptyMap: ',';
BlockBody: <Chain>?;
MapBody: <Mapping> (',' <Mapping>)*;
Mapping: <Expression> ':' <Expression>;
SetBody: <Expression> (',' <Expression>)*;

Parentheses: '(' <Expression> ')';

Brackets: '[' <ExpressionList> ']';

LogicalNot: 'not' <Prefix>;
BitwiseNot: '!' <Prefix>;
Negate: '-' <Prefix>;

Pub: 'pub' <Mod | Fn | Let | Var>;

Mod: 'mod' <Identifier> '{' <Chain> '}';

Fn: 'fn' <Identifier>? <FnDeclaration | FnMatch>;
FnDeclaration: ('(' (<Identifier>)? ')')? <ArrowBody | Block>;
ArrowBody: '=>' <Expression>;
BlockBody: '{' <Chain> '}';
FnMatch: '{' <MatchBody> '}';

Let: 'let' <VariablePattern>;
Var: 'var' <VariablePattern>;
VariablePattern: <GuardedPattern> '=' <Expression> <VariableElse>?;
VariableElse: 'else' <Expression>;

If: 'if' <Expression> <ThenExpression | Block> <ElseExpression>?;
ThenExpression: 'then' <Expression>;
ElseExpression: 'else' <Expression>;

Loop: 'loop' <Block> ('while' <Expression>)?;
While: 'while' <Expression> <Block>;
For: 'for' <GuardedPattern> 'in' <Expression> <Block>;

Labeled: <Label> ':' <Loop | While | For | Block>;

Continue: 'continue' <Label>?;
Break: 'break' <Label>? <Expression>?;
Return: 'return' <Expression>?;

Match: 'match' <Expression> '{' <MatchBody> '}';
MatchBody: (<MatchPattern> (',' <MatchPattern>)*)?;
MatchPattern: <GuardedPattern> '=>' <Expression>;
GuardedPattern: <Pattern> ('if' <Expression>)?;
Pattern: <PatternKind> ('|' <PatternKind>)*;
PatternKind: <IdentifierPattern | ListPattern | MapPattern | ExpressionPattern>;
IdentifierPattern: '_' | '...' | <Identifier>;
ListPattern: '[' (<Pattern> (',' <Pattern>)*)? ']';
ExpressionPattern: ('<=' | '>=' | '<' | '>' | '=' | '!=') <Expression>;

MapPattern: '{' (<EntryPattern> (',' <EntryPattern>)*)? ','? '}';
EntryPattern: <EntryKeyPattern> ':' <Pattern>;
EntryKeyPattern: <Identifier | Number | String | Symbol>;

Try: 'try' <Expression> ('catch' <MatchBlock | SingleCatch | ArrowCatch>)?;
SingleCatch: <GuardedPattern> <Block>;
ArrowCatch: '=>' <Expression>;

Throw: 'throw' <Expression>?;

Term: <Identifier | Number | Regex | String | Symbol>;

Lexical Analysis

Source code is interpretted as a series of tokens. Whitespace is insignificant to Muse, and any amount of whitespace is allowed between tokens. Comment tokens are also ignored by the Muse compiler. Many of these tokens use Muse's own Regex syntax to represent the allowed characters.

The types of tokens Muse supports are:

  • Identifier: Any valid Unicode Identifier is supported by Muse. Underscores are allowed anywhere in an identifier, and an identifier can be comprised solely of underscores. Ascii digits can be used anywhere except at the beginning of an identifier.

  • Number: <Integer | Float | Bits>.

    • Integer: \\d+u?/
    • Float: \(\d+\.\d*|\.\d+)/
    • Bits: <Hex | Octal | Binary | Radix>
      • Hex: \0u?x[\da-f]+/i
      • Octal: \0u?o[0-5]+/
      • Binary: \0u?b[01]+/
      • Radix: \\d+u?r[0-9a-z]+/i
  • Regex: Regex supports two formats of Regex literals, both which use \ to begin the Regex. The end of the Regex is /. E.g., \hello/ is a Regex that matches hello.

    The only character that needs to be escaped beyond what Regex normally requires is /. E.g., \hello\/world/ will match hello/world.

    • Expanded: w\ followed by an escaped Regex where whitespace is ignored, ended with /
    • Normal: \ followed by an escaped Regex, ended with /

    These characters can be added after the trailing / to customize the Regex options:

    • i: Ignore case
    • u: Enables Unicode mode for the entire pattern
    • s: . matches all, including newlines
    • m: Enables multiline matching

    Muse uses the regex crate, which provides excellent documentation about what features it supports.

  • String: Begins and ends with ". Escapes begin with \ and match Rust's supported escape sequences:

    • \": Quotation mark
    • \n: ASCII newline
    • \r: ASCII carriage return
    • \t: ASCII tab
    • \\: Backslash
    • \0: ASCII null
    • \x: ASCII escape. Must be followed by exactly two hexadecimal digits.
    • \u: Unicode escape. Must be followed by up to six hexadecimal digits enclodes in curly braces. E.g, \u{1F980} is the escape sequence for 🦀.
  • Symbol: : followed by a valid <Identifier>.

  • Label: @ followed by a valid <Identifier>.

  • Comment: # followed by any character until the next line.

Expressions

Program: <Chain>;

Chain: <Expression> (';' <Expression>)*;

Expression: <Assignment> | <ArrowFn> | <InlineIf>;

The compiler expects one or more expressions delimited by a ';'. These expressions are evaluated within the root "module". When executing multiple compiled programs in the same virtual machine, the root module will contain declarations from previous programs.

Each expression can be an assignment, an arrow function, or an inline if. The precedence of these operators matches the order they are listed in. For example, these two ways of assigning to my_function are identical:

var my_function = nil;

my_function = n => n * 2 if n > 0;
my_function = (n => (n * 2 if n > 0));

The definition of inline if recurses into the remaining expression types.

Values, Variables, and Assignment

Assignment: <Lookup | Index> ('=' <Assignment>)*;

Let: 'let' <VariablePattern>;
Var: 'var' <VariablePattern>;
VariablePattern: <GuardedPattern> '=' <Expression> <VariableElse>?;
VariableElse: 'else' <Expression>;

Named values can be declared using either the let or var keywords followed by a pattern binding.

When a let expression is evaluated, all bound identifiers will not be able to be reassigned to. In Muse, these are simply called values. While values cannot be reassigned to, they can be shadowed by another declaration.

# Basic value declaration
let a = 42;
# Values declared using destructuring
let (a, b, _) = [0, 1, 2];
# Handling pattern mismatches
let (a, b) = [0, 1, 2] else return;

# The contents of a value can still be affected (mutated), if the type supports it.
let a = [];
a.push(42);

If the expression does not match the pattern, either the else expression will be evaluated or a pattern mismatch exception will be thrown. The else expression requires that all code paths escape the current block. This can be done using the break, continue, return, or throw expressions.

When the var keyword is used, all bound identifiers become variables. Unlike values, variables can be assigned new values.

# Basic declaration
var a = 42;
# `var`s can have their values updated through assignment.
a = 42;

# Declarations using destructuring
var (a, b, _) = [0, 1, 2];

# Both a and b can have their values updated
a = 42;
b = 43;

Comparisons

Comparison:
    <BitwiseOr>
    <LessThanOrEqual |
        LessThen |
        Equal |
        NotEqual |
        GreaterThan |
        GreaterThanOrEqual>*;
LessThanOrEqual: '<=' <BitwiseOr>;
LessThan: '<' <BitwiseOr>;
Equal: '=' <BitwiseOr>`;
NotEqual: '!=' <BitwiseOr>;
GreaterThan: '>' <BitwiseOr>;
GreaterThanOrEqual: '>=' <BitwiseOr>;

Comparing two values in Muse is done using comparison operators. The types of comparisons Muse supports are equality and relative comparisons.

Equality Comparison

When Muse checks if two values are equal or not equal, Muse will try to approximately compare similar data types. For example, all of these comparisons are true:

let true = 1 == 1.0;
let true = 1 == true;
let true = 1.0 == true;

nil is only considered equal to nil and will be not equal to every other value.

let true = nil == nil;
let false = nil == false;

Relative Comparison

Muse attempts to create a "total order" of all data types so that sorting a list of values can provide predictable results even with mixed types. If a type does not support relative comparisons, its memory address will be used as a unique value to compare against.

Range Comparisons

Chaining multiple comparison expressions together creates a range comparison. For example, consider 0 < a < 5. On its own, 0 < a results in a boolean, which traditionally would result in the second part of the expression becoming bool_result < 5. In general, this isn't the programmer's desire.

Muse interprets 0 < a < 5 as 0 < a && a < 5. This form of chaining comparisons can mix and match all comparison operators. Consider a longer example:

let a = 1;
let b = 2;

# These two statements are practically identical
let true = 0 < a < b < 5;
let true = 0 < a && a < b && b < 5;

Boolean Logic

LogicalOr: <LogicalXor> ('or' <LogicalXor>)*;
LogicalXor: <LogicalAnd> ('xor' <LogicalAnd>)*;
LogicalAnd: <Comparison> ('and' <Comparison>)*;

LogicalNot: 'not' <Prefix>;

Boolean logic allows combining boolean values by performing logic operations. The logic operations that Muse supports are and, or, xor, and not.

Truthiness

Muse evaluates each operand's truthiness to perform boolean logic. If a value is said to be truthy, it is considered equivalent to true in boolean logic. If a value is said to be falsy, it is considered equivalent to false in boolean logic. Each type is responsible for implementing its truthiness conditions:

  • nil: Always falsey
  • Numbers: Non-zero values are truthy, zero is falsy.
  • Strings: Non-empty strings are truthy, empty strings are falsey.
  • Lists/Tuples/Maps: Non-empty collections are truthy, empty collections are falsey.
  • Symbol: Always truthy
  • Other types that don't implement truthy: Always truthy.

Logical Or

The logical or expression is a short-circuiting operator that returns true if either of its operands are truthy.

let true = true or true;
let true = true or false;
let true = false or true;
let false = false or false;

The short-circuiting behavior ensures that once the expression is known to return true, no remaining chained expressions will be evaluated:

# "error" is not evaluated
let true = true or error;

Logical Xor

The logical exclusive or (xor) expression is an operator that returns true if one of its operands is truthy, but not both.

let true = true xor false;
let true = false xor true;
let false = true xor true;
let false = false xor false;

This operator can not short-circuit, so both expressions are always evaluated.

Logical And

The logical and expression is a short-circuiting operator that returns true both of its operands are truthy.

let true = true or true;
let true = true or false;
let true = false or true;
let false = false or false;

The short-circuiting behavior ensures that once the expression is known to return false, no remaining chained expressions will be evaluated:

# "error" is not evaluated
let false = false and error;

Bitwise

BitwiseOr: <BitwiseXor> ('|' <BitwiseXor>)*;
BitwiseXor: <BitwiseAnd> ('^' <BitwiseAnd>)*;
BitwiseAnd: <AddSubtract> ('&' <AddSubtract>)*;

BitwiseShift: <AddSubtract> <ShiftLeft | ShiftRight>*;

ShiftLeft: '<<' <AddSubtract>;
ShiftRight: '>>' <AddSubtract>;

BitwiseNot: '!' <Prefix>;

Bitwise operations operate on integers using logic operations on each individual bit. In Muse, integers are 64-bits and can be signed or unsigned. Because signed numbers use a bit for determining the sign, it is often preferred to use unsigned numbers only when performing bitwise operations.

The only operator that performs differently between signed and unsigned integers is shift right. Muse uses a sign-preserving shift-right operation for signed integers.

These operators can be overridden by types to perform different non-bitwise functionality.

Bitwise Or

The bitwise or expression produces a new value by performing a logical or operation on each corresponding bit in the two operands.

let 0b101 = 0b100 | 0b001

Bitwise Xor

The bitwise excusive or (xor) expression produces a new value by performing a logical xor operation on each corresponding bit in the two operands.

let 0b101 = 0b110 ^ 0b011

Bitwise And

The bitwise and expression produces a new value by performing a logical and operation on each corresponding bit in the two operands.

let 0b100 = 0b110 & 0b101

Bitwise Shifting

The bitwise shift expressions produce a new value by moving bits left or right by a number of bits, filling in any empty bits with 0.

let 0b100 = 0b010 << 1;
let 0b001 = 0b010 >> 1;

The shift-right expression is sign-preserving when operating on signed integers:

let -2 = -4 >> 1;

Bitwise Not

The bitwise not expression produces a new value by performing a logical not operation on each bit in the operand.

let -1 = !0;
let 0uxffff_ffff_ffff_ffff = !0u;

Math/Arithmetics

AddSubtract: <MultiplyDivide> <Addition | Subtraction>*;
Addition: '+' <MultiplyDivide>;
Subtraction: '-' <MultiplyDivide>;

MultiplyDivide:
    <Power>
    <Multiply | Divide | Remainder | IntegerDivide>*;
Multiply: '*' <Power>;
Divide: '/' <Power>;
Remainder: '%' <Power>;
IntegerDivide: '//' <Power>;

Power: <Punctuation> ('**' <Punctuation>)*;

Negate: '-' <Prefix>;

Arithmetic Order of Operations

Arithmetic expressions in Muse honor the traditional order of operations. For example, consider these identical pairs of expressions written without and with parentheses:

2 * 4 + 2 * 3;
(2 * 4) + (2 * 3);

2 ** 3 * 2 + 1;
((2 ** 3) * 2) + 1;

Floating Point and Integer Arithmetic

With the exception of the division (/), integer divide (//) and remainder (%) operators, all math operators will follow these rules for type conversions:

  • If any operand is a Dynamic, return the result of invoking the associated function for the operator on that type.
  • If all operands are of the same numeric type, return the result as the same numeric type.
  • If any operand is a floating point, return the result as a floating point.
  • Perform the result as integers, using saturating operations, preserving the integer type of the first operand.

The division operator (/) always returns a floating point result when both operands are numbers. Conversely, the remainder (%) and integer divide (//) operators always return integer results when both operands are numbers.

Term

Term: <Identifier | Number | Regex | String | Symbol>;

Lookup/Call/Index

Punctuation: <Prefix> <Call | Index | Lookup | TryOperator | NilCoalesce>*

Call: '(' <ExpressionList> ')' <Punctuation>?;
Index: '[' <ExpressionList> ']' <Punctuation>?;
Lookup: '.' <Identifier> <Punctuation>?;

This set of operators acts on a term followed by a way of interacting with that term to either call it like a function, look up a value by index, or lookup a member value by an identifier.

These expression are chainable. For example, this code accesses a value from a list within a list by chaining the index operator after invoking list.get(0) function:

let list = [[1, 2, 3], [4, 5, 6]];
let 2 = list.get(0)[1];

Nil and Exception Handling

Punctuation: <Prefix> <Call | Index | Lookup | TryOperator | NilCoalesce>*

TryOperator: '?' <Punctuation>?;
NilCoalesce: '??' <Punctuation>;

Try: 'try' <Expression> ('catch' <MatchBlock | SingleCatch | ArrowCatch>)?;
SingleCatch: <GuardedPattern> <Block>;
ArrowCatch: '=>' <Expression>;

Throw: 'throw' <Expression>?;

Muse embraces exception handling for propagating errors and aims to make handling errors straightfoward when desired.

When an exception is thrown, Muse unwinds the execution stack to the nearest catch handler. If the catch handler matches the exception thrown, the exception handler will be executed. If not, Muse will continue to unwind the stack until a matching handler is found or execution returns to the embedding application. In Rust, the result will be Err(_).

Any value can be used as an exception, and handling specific functions is done using pattern matching. Consider this example:

fn first(n) {
    try {
        second(n)
    } catch :bad_arg {
        second(1)
    }
};

fn second(n) {
    try {
        100 / n
    } catch :divided_by_zero {
        throw :bad_arg
    }
};

first(0)

When the above example is executed, second is invoked with 0, which causes a divide by zero exception. The exception occurs within the try block, and is caught by the catch :divide_by_zero block.

The catch block proceeds to throw the symbol :bad_arg as a new exception. first() is executing second() within a try block that catches that value, and calls second(1) instead. This will succeed, and return a result of 100.

While this is a nonsensical example, it highlights the basic way that exception handling and pattern matching can be combined when handling errors.

Catch any error

Catching any error can be done in one of two ways: using an identifier binding or using an arrow catch block. In the next example, both functions are identical:

fn identifier_binding() {
    try {
        1 / 0
    } catch err {
        err
    }
}

fn arrow_catch() {
    try {
        1 / 0
    } catch => {
        # when not provided a name, `it` is bound to the thrown exception.
        it
    }
}

Matching multiple errors

To catch multiple errors raised by inside of a try block, a match block can be used:

fn checked_area(width,height) {
    if width == 0 {
        throw :invalid_width
    } else if height == 0 {
        throw :invalid_height
    }

    width * height
};

try {
    checked_area(0, 100)
} catch {
    :invalid_width => {}
    :invalid_height => {}
}

Converting any exception to nil

A try block without a catch block will return nil if an exception is raised. Similarly, the try operator (?) can be used to convert any exception raised to nil. These examples produce identical code:


try {
    1 / 0
};

(1 / 0)?;

Maps and Sets

BlockOrMap: '{' <EmptyMap | BlockBody | MapBody | SetBody> '}';
EmptyMap: ',';
BlockBody: <Chain>?;
MapBody: <Mapping> (',' <Mapping>)*;
Mapping: <Expression> ':' <Expression>;
SetBody: <Expression> (',' <Expression>)*;

Maps are collections that store key-value pairs. The underlying way maps store their key-value pairs is considered an implementation detail and not to be relied upon.

Map literals are created using curly braces ({ / }). An empty map literal is created by placing a comma (,) between the braces: {,}. Muse considers {} to be an empty block, which results in nil.

Pairs are specified by placing a colon (:) between two expressions. For example, consider this map literal and usage:

let map = {
    "a": 1,
    "b": 2,
    "c": 3,
};
let 2 = map["b"];

Sets in Muse are implemented under the hood using the Map type. Set literals can be specified by using a comma separated list of expressions in curly braces:

let set = {
    1,
    2,
    3
};
set.contains(1)

Lists/Arrays/Tuples

Brackets: '[' <ExpressionList> ']';

A sequence of values stored sequentially is a List in Muse. In other languages, these structures may also be referred to as arrays or tuples.

let list = [1, 2, 3];
$assert(list[0] == 1);
$assert(list[1] == 2);
$assert(list[2] == 3);

Functions

Fn: 'fn' <Identifier>? <FnDeclaration | FnMatch>;
FnDeclaration: ('(' (<Identifier>)? ')')? <ArrowBody | Block>;
ArrowBody: '=>' <Expression>;
BlockBody: '{' <Chain> '}';
FnMatch: '{' <MatchBody> '}';

Return: 'return' <Expression>?;

Functions in Muse are parameterized expressions that can be executed on-demand. This is the primary way to reuse code and avoid code duplication.

fn Functions

There are many forms that fn functions can be declared.

Anonymous Functions

Anonymous functions do not have a name specified at the time of creation. They can only be called by calling the function returned from the function expression:

let square = fn(n) => n ** 2;
let 4 = square(2);

let area = fn(width, height) => width * height;
let 6 = area(2, 3);

# Function bodies can also be specified using a block.
let square = fn(n) {
    n ** 2
};
let 4 = square(2);

let area = fn(width, height) {
    width * height
};
let 6 = area(2, 3);

Named Functions

If an identifier is directly after the fn keyword, the function is declared using the identifier as its name. These examples are identical to the anonymous function examples, except they utilize named functions instead of let expressions to declare the function.

fn square(n) => n ** 2;
let 4 = square(2);

fn area(width, height) => width * height;
let 6 = area(2, 3);

# Function bodies can also be specified using a block.
fn square(n) {
    n ** 2
};
let 4 = square(2);

fn area(width, height) {
    width * height
};
let 6 = area(2, 3);

Match Functions

Muse supports function overloading/multiple dispatch using pattern matching. If an open curly brace ({) is found after the fn keyword or after the function's name, the contents of the braces are interpretted as a set of match patterns:

fn area {
    [width] => width * width,
    [width, height] => width * height,
};

let 4 = area(2);
let 6 = area(2, 3);

# Anonymous functions can also be match functions
let area = fn {
    [width] => width * width,
    [width, height] => width * height,
};

let 4 = area(2);
let 6 = area(2, 3);

Returning values from a function

When a function executes, the result of the final expression evaluated will be returned. The return expression can be used to exit a function without executing any additional code.

fn my_function() {
    return;
    this_expression_wont_be_evaluated
};
my_function()

The return expression can also be provided an expression to return as the result of the function. If no value is provided, nil is returned.

fn checked_op(numerator, denominator) {
    if denominator == 0 {
        return numerator // denominator;
    }
};
let nil = checked_op(1, 0);
let 3 = checked_op(6, 2);

Modules

Mod: 'mod' <Identifier> '{' <Chain> '}';

Pub: 'pub' <Mod | Fn | Let | Var>;

Modules provide a way to encapsulate code behind a namespace. Consider this example:

mod math {
    pub fn square(n) => n * n;
};

math.square(4)

The above example creates a module named math, which contains a function named square. The function is then invoked with the value 4.

Publishing declarations

The pub keyword can be used to publish modules, functions, values, and variables. Without the pub keyword in the previous section's example, attempting to access the square function will fail.

The default visibility of a declaration is module-private. Any other module, including submodules, are not be able to access private declarations.

Published declarations are able to be accessed anywhere that the containing module is accessible.

If

If: 'if' <Expression> <ThenExpression | Block> <ElseExpression>?;
ThenExpression: 'then' <Expression>;
ElseExpression: 'else' <Expression>;

InlineIf: <LogicalOr> ('if' <LogicalOr> ('else' <Expression>)?)?;

The if keyword allows conditionally evaluating code. There are two forms of the if expression: standalone and inline.

Standalone If

The standalone if expression enables executing an expression when a condition is true:

let 42 = if true {
    42
};
let 42 = if true then 42;
let nil = if false {
    42
};
let nil = if false then 42;

If the else keyword is the next token after the "when true" expression, an expression can be evaluated when the condition is false.

let 42 = if false {
    0
} else {
    42
};
let 42 = if false then 0 else 42;

Because if is an expression, the expressions can be chained to create more complex if/else-if expressions:

fn clamp_to_ten(n) {
    if n < 0 {
        0
    } else if n > 10 {
        10
    } else {
        n
    }
};
let 0 = clamp_to_ten(-1);
let 1 = clamp_to_ten(1);
let 10 = clamp_to_ten(11);

Inline If

An inline if expression returns the guarded expression if the condition is true, or nil when the condition is false:

let 42 = 42 if true;
let nil = 42 if false;

Similar to the standalone if expression, else can be used to execute a different expression when the condition is false:

let 42 = 0 if false else 42;

Inline if statements can also be chained:

fn clamp_to_ten(n) {
    0 if n < 0
        else 10 if n > 10
        else n
};
let 0 = clamp_to_ten(-1);
let 1 = clamp_to_ten(1);
let 10 = clamp_to_ten(11);

Pattern Matching

Match: 'match' <Expression> '{' <MatchBody> '}';
MatchBody: (<MatchPattern> (',' <MatchPattern>)*)?;
MatchPattern: <GuardedPattern> '=>' <Expression>;
GuardedPattern: <Pattern> ('if' <Expression>)?;
Pattern: <PatternKind> ('|' <PatternKind>)*;
PatternKind: <IdentifierPattern | ListPattern | MapPattern | ExpressionPattern>;
IdentifierPattern: '_' | '...' | <Identifier>;
ListPattern: '[' (<Pattern> (',' <Pattern>)*)? ']';
ExpressionPattern: ('<=' | '>=' | '<' | '>' | '=' | '!=') <Expression>;

MapPattern: '{' (<EntryPattern> (',' <EntryPattern>)*)? ','? '}';
EntryPattern: <EntryKeyPattern> ':' <Pattern>;
EntryKeyPattern: <Identifier | Number | String | Symbol>;

match expression

The match expression enables concise and powerful ways to inspect and instract data from the result of an expression.

Match block

A match block is one or more guarded patterns and their associated expressions. Any identifiers bound in the match pattern will only be accessible while executing that pattern's guard and associated expression.

When evaluating a match block, the compiler generates code that tries to match each pattern in sequence. If no match is found, a pattern mismatch exception is thrown.

Guarded Pattern

A guarded pattern is a pattern followed by an optional if condition. If the pattern matches, the if condition is evaluated. If the result is truthy, the pattern is considered a match.

Pattern

A pattern can one one of these kinds:

  • Wildcard: _ will match any value.
  • Named wildcard: Any identifier
  • Remaining Wildcard: ... will match all remaining elements in a collection/
  • An expression comparison: A comparison operator followed by an expression.
  • Tuple pattern: A comma separated list of patterns enclosed in parentheses.
  • List pattern: A comma separated list of patterns enclosed in square brackets.

Multiple matching patterns can be used by chaining patterns together with the vertical bar (|). This example uses a match function to try to demonstrate a lot of the flexibility this feature provides:

fn test_match {
    [a, b] if b != 1 => a - b,
    [a, b] => a + b,
    n => n,
    [] => 42,
    _ => "wildcard",
};

let 42 = test_match(44, 2);
let 42 = test_match(41, 1);
let 42 = test_match(42);
let 42 = test_match();
let "wildcard" = test_match(1, 2, 3);

Loops

Loop: 'loop' <Block> ('while' <Expression>)?;
While: 'while' <Expression> <Block>;
For: 'for' <GuardedPattern> 'in' <Expression> <Block>;

Muse has four loop types:

  • Infinite loop: Repeats the loop body unconditionally.
  • While loop: Repeats the loop body while a condition is truthy.
  • Loop-While loop: Executes the loop body and then repeats the loop while the condition is truthy.
  • For loop: Evaluates the loop body for each matching element in an iterable value.

Each loop type currently requires that the loop body is a block. All loop's execution can be affected by these operations:

  • An uncaught exception
  • return
  • break: Exits the loop body, optionally returning a value.
  • continue: Jumps to the beginning of the next loop iteration, which is just prior to the iterator is advanced and any conditions being checked.

Infinite Loop

var n = 0;
let nil = loop {
    if n % 2 == 0 {
        n = n + 1;
    } else if n > 50 {
        break;
    } else {
        n = n * 2;
    }
};
let 63 = n;

The above loop executes a series of operations until n > 50 is true, at which point the loop exits.

While Loop

A while loop checks that a condition is truthy before executing the loop body, and continues repeating until the condition is not truthy.

var n = 0;
while n < 10 {
    n = n + 1;
};
let 10 = n;

Loop-While Loop

A Loop-While loop executes the loop body, then checks whether the condition is truthy. This is different from a While loop in that the condition is only checked after the first iteration of the loop. The continue expression will continue iteration just prior to the condition evaluation.

var n = 1;
loop {
    n = n * 3;
} while n % 4 != 1;
let 9 = n;

For loop

A For loop iterates a value and executes the loop body for each matching element in the iterator. The syntax for the for loop uses a guarded pattern, which means all pattern matching features can be used in the for loop syntax.

If the iterator returns an item that does not match the pattern, the next element will be requested and the loop body will not be executed for that element.

var sum = 0;
for n in [1, 2, 3] {
    sum = sum + n
};
let 6 = sum;

for (key, value) in {"a": 1, "b": 2} {
    match key {
        "a" => let 1 = value,
        "b" => let 2 = value,
    }
};

var sum = 0;
for (_, value) if value % 2 == 1 in {"a": 1, "b": 2, "c": 3} {
    sum = sum + value;
};
let 4 = sum;

Break, Continue, and Labels

Labeled: <Label> ':' <Loop | While | For | Block>;
Continue: 'continue' <Label>?;
Break: 'break' <Label>? <Expression>?;

Muse does not have a "go to" capability, but it offers several expressions that can jump to well-defined execution points: continue and break.

The well-defined execution points are:

  • A loop's next iteration
  • The first instruction after a loop body
  • A labeled block or loop

Labels

A label is the @ character followed by an identifier, e.g., @label. Labels can be applied to blocks or loops to allow controlled execution flow in nested code.

var total = 0;
@outer: for x in [1, 2, 3] {
    for y in [1, 2, 3] {
        if x == y {
            continue @outer;
        };

        # Only executed for [(2, 1), (3,1), (3, 2)]
        total = total + x * y
    };
};
let 11 = total;

Continue

The continue expression takes an optional label. If no label is provided, execution jumps to the location to begin the next iteration of the loop containing the code. If no loop is found, a compilation error will occur.

If a label is provided, the label must belong to a loop. If not, a compilation error will occur.

Break

The break expression takes an optional label and an optional expression. If no label is provided, execution jumps to the location after the current loop exits. If no loop is found, a compilation error will occur.

If a label is provided, execution will jump to the the next instruction after the block or loop identified by that label. The result of the block or loop will be the optional expression or nil.

Why Muse over ...?

Muse's main distinguishing factor is that it attempts to provide a safe way to execute untrusted code, including code that may never terminate.

All languages have different design goals, performance, and developer experiences. In the end, there are many viable options for embedded languages. The choice of what language to embed is a highly personal decision.

Why use Muse over WASM?

Overall, WASM is a very tempting option for safely executing untrusted code within another application. Embedding WASM allows users to run arbitrary code in isolated environments incredibly efficiently and safely. Many runtimes also support budgeted execution to allow developers to defend against inefficient code or infinite loops. It used to be a fair amount of work to set up a plugin system using WASM, but Extism makes it incredibly easy today.

The downsides might be:

  • Large number of dependencies. Let's face it, build times can sometimes be an issue. Muse has limited dependencies, and due to being a very focused implementation, it means Muse will always be less code than embedding a WASM runtime.
  • Serialization Overhead. With Muse, your Rust types can be used directly within the scripts with no serialization overhead. With WASM, because memory isn't guaranteed to be compatible, serialization is often used when passing data between native code and WASM.
  • Split documentation. With every language that your plugin system supports, you might feel the need to provide documentation for those languages. By focusing on a single embedded language, the end-user documentation may be less work to maintain.

Ultimately if the allure of having users be able to write code in the language they are most familiar with, WASM is a great option.