Basic Syntax & Semantics of mimium #
This section explains the basic syntax and semantics of the mimium language.
Comments #
As in Rust, C++, and JavaScript, anything to the right of //
is treated as a comment.
You can also use /* */
to comment out multiple lines.
Variable Declaration and Assignment #
To create a variable, use the let
keyword followed by a name, =
, and the value you want to assign.
let mynumber = 1000
If a variable with the same name already exists in the scope, the most recent let
declaration in that scope will be referenced (without affecting the original variable). This is called shadowing.
fn dsp(x){
let x = 1.0
x // x is always 1.0, regardless of the argument provided
}
Assigning a value to a variable without let
updates the existing variable.
let mynumber = 1000
mynumber = 2000 // 2000 is newly assigned to mynumber
In mimium, variables created with let
are mutable by default (destructive assignment is always allowed). However, because mimium’s function evaluation is based on call-by-value, destructive assignment to function arguments does not affect values outside the function.
Since mimium does not have imperative constructs like for
loops, there is rarely a need to actively use destructive assignment.
However, closures can capture variables and allow limited read/write access outside the function.
Types #
Types are concepts used to distinguish data like numbers or strings based on their purpose.
mimium is a statically-typed language, meaning all types are determined at compile time (before producing sound).
Statically-typed languages generally have better execution speed than dynamically-typed languages. While manually specifying types can make code verbose, mimium supports type inference, which automatically determines types from context, keeping code concise.
There are primitive types (smallest indivisible units) and aggregate types (combinations of multiple types).
Explicit type annotations can be added during variable and function declarations. Use :
(colon) after the variable or parameter name to specify the type.
let myvar:float = 100
If you assign a different type, a compile-time error occurs:
let myvar:string = 100
In functions, return types are specified after the parameters, using ->
.
fn add(x:float,y:float)->float{
x + y
}
In this add
function, type annotations for x
and y
can be omitted due to context inference1:
fn add(x,y){
x+y
}
Primitive Types #
mimium supports the following primitive types: float
, string
, and void
.
float
: Represents numbers (internally as 64-bit floats). To work with integers, use functions likeround
,ceil
, orfloor
.string
: Created with double quotes (e.g.,"hoge"
). Strings are currently limited to:- Debugging with
make_probe
- Loading audio files with
make_sampler
- Including other source files with
include
- Debugging with
void
: Indicates a function has no return value.
Aggregate Types #
Function Types #
Function types are denoted as (T1,T2,...)->T
.
Tuples #
Tuples group different types together. They are created with ()
and comma-separated values:
let mytup = (100,200,300)
Values can be extracted by placing comma-separated variables on the left-hand side:
let (one,two,three) = mytup
To annotate tuple types explicitly:
let (one,two,three):(float,float,float) = mytup
In future versions, accessing tuple elements by index (e.g., mytup.1
) will be implemented.
Functions #
Functions encapsulate reusable procedures that take inputs and return outputs.
fn add(x,y){
x+y
}
Functions are first-class in mimium, meaning they can be assigned to variables or passed as arguments.
let my_function:(float,float)->float = add
Anonymous Functions (Lambdas) #
Anonymous functions can be created and assigned to variables:
let add = |x:float,y:float|->float {x+y}
They can also be called directly:
println(|x,y|{x + y}(1,2)) // prints "3"
Pipe (|>
) Operator
#
In mimium, the pipe operator |>
allows you to transform nested function calls like a(b(c(d)))
into d |> c |> b |> a
.
The pipe operator has lower precedence than any other operator. Line breaks are allowed before and after the pipe. When combined with partial application, it can clearly express data flow.
Currently, the pipe operator only works with functions that take a single parameter. Future updates will support unpacking tuples for functions with two or more parameters using features like parameter packs.
Partial Application with Underscore (_
)
#
You can create a new function by using an underscore (_
) in place of an argument during function application. For example, to create a new function addone
that fixes one argument of the add
function to 1:
let addone = add(_,1)
This is implemented as syntactic sugar, equivalent to the following:
let addone = |lambda_a1| add(lambda_a1,1)
When combined with the pipe operator, it can express data flow like this:
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)
}
Line breaks are allowed immediately before and after the pipe operator.
Loops with Recursion #
Named functions can call themselves, allowing for recursion.
The fact
function, which calculates the factorial, can be defined as follows:
fn fact(input:float){
if(input>0) 1 else input * fact(input-1)
}
Be cautious when using recursive functions, as they may lead to infinite loops.
letrec
#
Recursion is allowed only for top-level function definitions. It cannot be expressed with let
and lambda expressions. To define a recursive function within a nested function, use letrec
instead of let
.
letrec fact = |input|{
if(input>0) 1 else input * fact(input-1)
}
This is internally equivalent to the fn
syntax. Note that variables declared with letrec
cannot use patterns like tuple unpacking, which is possible with let
.
Closures #
TBD
Expressions, Statements, and Blocks #
A collection of statements enclosed in curly braces {}
is called a block. A statement usually consists of assignments using expressions, such as let a = b
or x = y
. An expression can be a number like 1000
, a variable symbol like mynumber
, an arithmetic expression like 1+2*3
, or a function call like add(x,y)
.
A block is actually a type of expression. A block can contain multiple statements, and the last expression in the block is its return value.
// mynumber should be 6
let mynumber = {
let x = 2
let y = 4
x+y
}
Conditional #
mimium uses the if (condition) then_expression else else_expression
syntax for conditionals.
condition
, then_expression
, and else_expression
are all expressions. If the value of condition
is greater than 0, the then_expression
is evaluated; otherwise, the else_expression
is evaluated.
You can express the then
/else
parts as blocks, like this:
fn fact(input:float){
if(input>0){
1
}else{
input * fact(input-1)
}
}
Since the if syntax is an expression, the same code can be written more simply:
fn fact(input:float){
if (input>0) 1 else input * fact(input-1)
}
include #
You can use the include("path/to/file.mmm")
syntax to load other files within the current file.
If the file path is an absolute path, that path is used. If it’s a relative path, mimium first searches the standard library (~/.mimium/lib
), and if not found, it searches relative to the current file’s location.
Currently, there is no separation of namespaces for included files; the include
statement simply replaces itself with the content of the included file. Be cautious of infinite loops when including files that depend on each other.
BNF Grammar Definitions and Operator Precedence #
TBD
Arithmetic operators like
+
and*
only apply to numeric types. This may change in future updates. ↩︎