Changes in V3

Language Spec Changes in mimium v3 #

mimium v3, a programming language for music, includes significant updates to both language specifications and runtime. While the language specification itself maintains backward compatibility, some built-in functions and libraries introduce breaking changes.

The major changes include the introduction of record types, parameter packs for flexible function calls, multi-stage computation (type-safe macros/compile-time computation), and live coding support.

Non-Breaking Changes (New Features) #

Record Type and Syntax #

Basic Record Types (#99, #128) #

mimium v3 introduces record types (similar to structs in other languages) with a syntax inspired by Elm. Records allow you to group related data together with named fields, making code more readable and maintainable.

Basic Syntax:

// Record literal with named fields
let myadsr_param = { 
   attack = 100.0,
   decay = 200.0,
   sustain = 0.6,
   release = 2000.0,
}

// Type annotations are optional
let myrecord = {
  freq:float = 440.0,
  amp = 0.5,
}

// Single-field records require trailing comma
let singlerecord = {
  value = 100,
}

Accessing Record Fields:

// Dot operator for field access
let attack_time = myadsr_param.attack

// Pattern matching in let bindings
let {attack, decay, sustain, release} = myadsr_param

// With partial application using underscore
let myattack = myadsr_param |> _.attack

Record Update Syntax (#158):

Creating modified versions of records is a common pattern in functional programming. mimium v3 introduces a clean syntax for updating records:

let myadsr = { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// Update specific fields while keeping others unchanged
let newadsr = { myadsr <- attack = 4000.0, decay = 2000.0 }
// newadsr is { attack = 4000.0, decay = 10.0, sustain = 0.7, release = 10.0 }

// Original record remains unchanged (immutable semantics)
// myadsr is still { attack = 0.0, decay = 10.0, sustain = 0.7, release = 10.0 }

The record update syntax is implemented as syntactic sugar and ensures functional programming semantics - creating new records rather than modifying existing ones.

Parameter Pack and Default Parameters #

Parameter Pack (#130) #

Parameter packs allow functions to accept tuples or records as arguments, automatically unpacking them into individual parameters. This feature greatly improves the usability of functions with multiple parameters.

With Tuples:

fn add(a:float, b:float)->float {
  a + b
}

// Direct call with individual arguments
add(100, 200)  // Returns 300

// Automatic unpacking of tuples
add((100, 200))  // Returns 300

// Works seamlessly with pipe operators
(100, 200) |> add  // Returns 300

With Records:

fn adsr(attack:float, decay:float, sustain:float, release:float)->float {
  // ADSR envelope implementation...
}

// Call with a record - fields can be in any order
let params = { attack = 100, decay = 200, sustain = 0.7, release = 1000 }
adsr(params)

// Or inline
adsr({ decay = 200, attack = 100, release = 1000, sustain = 0.7 })

This feature is particularly useful for audio processing functions that often have many parameters, allowing you to organize parameters as records for better clarity.

Default Parameters (#134) #

Functions can now specify default values for parameters, reducing boilerplate when calling functions with common configurations:

fn foo(x = 100, y = 200) {
  x + y + 1
}

fn bar(x = 100, y) {
  x + y
}

fn dsp() {
  // Use empty record {} to accept all defaults
  foo({}) +           // Uses x=100, y=200, returns 301
  bar({y = 300})      // Uses x=100, y=300, returns 400
  // Total: 701
}

Default values work with parameter pack syntax. You can use {} to accept all default values and specify only the parameters you want to override. Currently, this works for direct function calls (non-closure, non-higher-order function contexts).

Multi-Stage Computation (Hygienic Macro/Compile-time Computation) (#135) #

One of the most powerful additions to mimium v3 is multi-stage computation, inspired by MetaOCaml, Scala 3 macros, and SATySFi. This allows you to write macros that generate code at compile-time while maintaining type safety.

Motivation #

Audio processing often involves two distinct phases:

  1. Building the computation graph (structure)
  2. Running the computation graph (processing samples)

Previously, parametric audio graphs required executing higher-order functions in the global environment before using them. Multi-stage computation makes this more natural and efficient.

Basic Syntax #

Multi-stage computation divides computation stages into 0(macro) and 1(main), and adds syntax for moving between each stage and a type system that includes types for the next stage as seen from stage 0 (Code type). This approach allows what are called macros to be based on the same lambda calculus as the code that runs directly at runtime, rather than manipulating source code or syntax trees. The most significant feature is that type checking and inference are completed before macro expansion.

Two primitives enable multi-stage computation. Quote (`expr) marks an expression for evaluation in the next stage. Splice ($expr) evaluates and inserts an expression from the previous stage. The macro expansion syntax (f!(args)) is similar to Rust’s macro calls, but in mimium it is implemented as syntactic sugar for ${f(args)}.

Example - Parametric Filter Bank:

Originally, parametric signal processing graph generation using higher-order functions required executing the higher-order function once in the global environment before use. This is because signal processing memory is allocated for the first time when the higher-order function is executed, which means generating a new processor every sample.

fn bandpass(x,freq){
      //...
    }
fn filterbank(n,filter_factory:()->(float,float)->float){
  if (n>0){
    let filter = filter_factory() 
    let next = filterbank(n-1,filter_factory)
    |x,freq| filter(x,freq+n*100)
             + next(x,freq)
  }else{
    |x,freq| 0
  }
}
let myfilter = filterbank(3,| | bandpass)//Graph generation here
fn dsp(){
      myfilter(x,1000)
}

Using macros, you can directly embed defined higher-order processors into the dsp function.

#stage(main)
fn bandpass(x, freq) {
  // bandpass filter implementation...
}
#stage(macro)
fn filterbank(n, filter:`(float,float)->float) -> `(float,float)->float {
  if (n > 0) {
    `{ |x,freq| 
       filter(x, freq + n*100) + filterbank!(n-1, filter)(x, freq)
    }
  } else {
    `{ |x,freq| 0 }
  }
}
#stage(main)
fn dsp() {
  filterbank!(3, `bandpass)(input, 1000)
  // This generates an unrolled filterbank at compile-time
}

Global Stage Declaration Syntax (#154) #

As you move between stages while maintaining variable scope, the nesting of quotes and splices becomes increasingly deep.

To avoid deeply nested brackets, mimium v3 introduces global-level stage declarations:

#stage(macro)
// Everything here is evaluated at compile-time (Stage 0)
fn mymacro(n:float) -> `float {
  `{ $n * 2.0 }
}
let compile_time_constant = 42

#stage(main)
// Everything here is evaluated at runtime (Stage 1)
fn dsp() {
  mymacro!(21)  // Results in 42.0
}

Live State Updating (#159) #

mimium v3 introduces a groundbreaking live coding feature that allows you to modify code while it’s running without interrupting audio playback.

How It Works #

With the introduction of multi-stage computation, most memory layouts required for signal processing can now be determined at compile time. By statically analyzing function calls from the dsp function, delay calls, use of self, etc., the system operates to preserve the internal state of delays and feedback as much as possible even when changes are made to the source code.

The new mimium-cli automatically detects file changes when you save an overwritten mimium file and updates the DSP with the following steps:

  1. The compiler recompiles the code in the background
  2. The new VM is compared with the running VM
  3. Creates a tree of internal states (delays, feedback, etc.), extracts parts with no structural changes, and creates internal state memory for the new VM
  4. The audio thread seamlessly switches to the new code

The system provides several key advantages. First, there is no audio interruption—delay tails and reverb continue naturally. When there are no changes to the internal state tree structure, such as simply changing constants, you only need to copy the memory wholesale, so there’s no need to compare all syntax trees. Furthermore, it can naturally follow changes to audio graphs due to editing compile-time constants, such as changing the filter order with macro arguments in the filterbank example from multi-stage computation.

This unique approach involves live coding functionality that recompiles the entire source code each time to generate new bytecode and VM instances while not interrupting the sound.

With this feature addition, mimium has acquired what could be called a unique capability among many music programming languages—the ability to live code very low-level DSP coding without audible interruption.

Example Use Case:

// Initial code
fn dsp() {
  let freq = 440.0
  sin(phasor(freq) * 3.14159 * 2.0)
}

// You can change the frequency while running:
fn dsp() {
  let freq = 880.0  // Just edit and save - no restart needed, even works with modulation and other internal states
  sin(phasor(freq) * 3.14159 * 2.0)
}

Even more complex changes work, including adding new oscillators, changing filter parameters, modifying delay times, and restructuring the audio graph.

The implementation uses a state_tree crate for state management with static memory allocation for deterministic state location. It detects insertions, deletions, and replacements in the call tree with minimal data copying for maximum efficiency.

Language Server (#151) #

mimium v3 includes a new Language Server Protocol (LSP) implementation, providing IDE features for better developer experience.

The language server provides semantic highlighting with proper syntax highlighting based on the compiler’s tokenizer and real-time error checking and reporting as you type. VSCode integration works with the mimium VSCode extension v2.3+.

The implementation is built on the tower-lsp framework and reuses mimium’s existing compiler infrastructure. Auto-completion and go-to-definition are planned for future implementation.

GUI and Tooling Improvements #

Slider UI (#147) #

A new Slider! macro provides easy-to-use GUI controls for audio parameters:

fn dsp() {
    let gain = Slider!("gain", 0.5, 0.0, 1.0)  // name, default, min, max
    let freq = Slider!("frequency", 440.0, 20.0, 20000.0)
    
    sin(phasor(freq) * 3.14159 * 2.0) * gain
}

The slider values are available in real-time and can be adjusted while the audio is running.

Code Formatter (#143) #

An experimental code formatter (mimium-fmt) has been added:

mimium-fmt myfile.mmm

The formatter provides consistent code formatting based on the AST and uses the pretty crate for layout. It is integrated with the updated parser (chumsky v0.10.x). However, comments are removed during formatting since trivia is not preserved in the AST yet, so consider this experimental for now.

Breaking Changes #

Audio File Loading: gen_sampler_monoSampler_mono! (#161) #

The runtime gen_sampler_mono function has been replaced with a compile-time Sampler_mono! macro.

Old Syntax:

let mysampler = gen_sampler_mono("audio_file.wav")
fn dsp() {
  mysampler(phase)
}

New Syntax:

fn dsp() {
  let phase = phasor(1.0)
  Sampler_mono!("audio_file.wav")(phase)
}

Audio files are loaded at compile-time and embedded directly in the VM’s data section, providing better performance with no runtime file I/O. The system includes automatic caching and resource management, with files cached and reused across recompilations.

Probe Function: make_probeProbe! (#156) #

The make_probe function has been replaced with the Probe! macro for consistency with other GUI features.

Old Syntax:

let myprobe = make_probe("test")
fn dsp() {
    osc(440) |> myprobe
}

New Syntax:

fn dsp() {
   osc(440) |> Probe!("test")
}

Both changes align with mimium v3’s philosophy of using multi-stage computation to move more work to compile-time, resulting in better performance and cleaner syntax.

Summary #

mimium v3 brings significant improvements to the language. Record types provide better data organization while parameter packs make functions more flexible and easier to use. Multi-stage computation enables powerful compile-time metaprogramming, and live state updating enables true live coding without audio interruption. The language server improves the development experience, and improved tooling with GUI macros and formatting enhances productivity. These features work together to make mimium more expressive, efficient, and enjoyable to use for audio programming while maintaining backward compatibility for most existing code.