Changes in V2

Language Spec Changes in mimium v2 #

Based on reflections from previous development, mimium has been redesigned from scratch.

The implementation language for the compiler and runtime environment has been changed from C++ to Rust.

By redesigning the language’s fundamental semantics and defining a VM in Rust, the range of expressibility has greatly expanded. However, because the bytecode interpreter is executed in Rust rather than using JIT compilation with LLVM, the overall execution performance is slower than before. This will be improved by implementing a JIT compiler for the bytecode in the future. Even with the current implementation, performance is sufficient for tasks like duplicating about 100 sine wave oscillators.

With the new language specification design, it is now possible to compile code that uses higher-order functions to replicate stateful functions (those that indirectly call self or delay) for signal processing.

let pi = 3.14159265359
let sr = 44100.0
fn phasor(freq){
  (self + freq/sr)%1.0
}
fn osc(freq){
  sin(phasor(freq)*pi*2.0)
}
fn amosc(freq,rate){
  osc(freq + osc(rate)*4000.0)
}
fn replicate(n,gen:()->(float,float)->float){
        let g = gen()
    if (n>0.0){
        let c = replicate(n - 1.0,gen)
        |x,rate| g(x,rate) + c(x+100.0,rate+0.1)
    }else{
        |x,rate|  0.0
    }
}
let n = 40.0
let mycounter = replicate(n,| |amosc);
fn dsp(){
    let res = mycounter(4000.0,0.5) / n
    (res,res)
}

Breaking Changes in Syntax #

Introduction of the let Keyword #

In mimium v2, several changes have been made to the surface-level language specification (syntax). For example, in previous versions of mimium, the same syntax was used for variable declaration and destructive assignment.

hoge = 10.0 // new variable declaration
hoge = 20.0 // destructive assignment

In v2, similar to Rust, you declare variables using the let keyword and perform assignments without let:

let hoge = 10.0
hoge = 20.0

Although this may seem to complicate the syntax, declaring variables with let clarifies the language’s semantics. Within nested function definitions, let allows for local variable declarations and shadowing, making it easier to reuse short, simple variable names contextually.

Removal of the return Keyword #

Up to mimium v0.4, the return keyword was required to specify return values in fn function definitions.

fn countup(active){
    return if (active) (self+1) else 0
}

From mimium v2 onward, the last expression in a block is the return value. This change, similar to Rust, removes the redundancy of the return keyword since early returns (e.g., via loops) are not needed in mimium.

fn countup(active){
    if (active) (self+1) else 0
}

Changes to Audio File Loading #

Previously, audio files were loaded using loadwav and loadwavsize. This has been changed to a pattern using the higher-order function gen_sampler_mono.

// gen_sampler_mono returns a higher-order function that takes playback position in samples as an argument
let sampler = gen_sampler_mono("myfile.wav")
fn counter(){
    self+1.0
}
fn dsp(){
    counter() |> sampler
}

Audio decoding uses the Symphonia crate in Rust. Therefore, all audio file formats supported by Symphonia, such as MP3 and FLAC, can be loaded.

Features in v0.4.0 Not Yet Implemented in v2.0.0 #

  • Struct types and type aliases are not implemented. This is a priority issue related to introducing polymorphism (generics). Proposals for record types are under consideration: GitHub Issue #99.
  • Array definitions and access syntax are not implemented due to issues related to internal array representation (array vs. list) and memory management.

Non-Breaking Changes (New Features) #

Addition of letrec Declarations #

In mimium, recursive function calls are allowed only within functions declared with fn. Inline functions declared with let myf = |x| { ... } do not allow recursion. In v2, fn declarations cannot be nested, making it impossible to write recursive functions in nested functions.

To address this, letrec allows inline recursive function definitions:

letrec myf = |x| { if(x>0.0) myf(x-1.0)+1.0 else 0.0 }

Unlike let, which allows multiple bindings (e.g., let (a,b) = tuple_value), letrec binds only a single variable.

Placeholder _ for Partial Application #

Using an underscore _ as an argument in a function application (or basic binary operator) creates a new function with that part as a new argument. This is implemented as syntactic sugar, making the following two syntaxes equivalent:

let f = foo(1.0, _, _)
let f = |a1,a2| foo(1.0,a1,a2)

Combining partial application with the pipe operator (|>) simplifies data flow expressions:

fn foo(x, y, z) {
    100.0 * x + 10.0 * y + z
}
let d2 = _ / _
let f = foo(1.0, _, 3.0)
fn dsp(){
    let x = 3.0 |>
        1.0 + _ |>
        d2(_, 2.0) |>
        f

    let y = 3.0
        |> 1.0 + _
        |> |arg| d2(arg, 2.0)
        |> f

    (x, y)
}

Currently, partial application creates and frees closures at runtime, causing slight performance overhead. This can be resolved with optimizations like function inlining and constant folding.

Future updates will add syntax for expanding arguments with tuples (parameter packs) using the pipe operator, alongside the introduction of record types.

(c) mimium development community(2024)