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 aResult<Value, Fault>
AsyncRustFunction
: A function that returns a type that implementsFuture<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.