Marmot
A statically typed, expression-oriented functional language with algebraic data types, exhaustive pattern matching, typeclasses, and explicit modules. Bindings stay fixed; reference cells hold changing state. Its compiler specializes generics and optimizes a typed SSA representation before emitting bytecode for the Marmot Virtual Machine. Workers run in separate VMs and communicate through typed channels.
Features
- 01
Static Type System
Bidirectional inference, generics, where constraints, and associated types. Generic functions specialize to concrete types during lowering.
- 02
Pattern Matching
Exhaustive matching with nested constructors, tuples, arrays with rest patterns, named record fields, integer ranges, alternatives, and guards.
- 03
Algebraic Data Types
Records and unions, transparent aliases, distinct newtypes, record update, and structural or container deriving.
- 04
Typeclasses
Constrained generics, associated types, and instance dispatch for operators, indexing, iteration, and value transfer.
- 05
Bindings & References
Bindings stay fixed. Explicit Ref<T> cells hold changing state: ref creates a cell, * reads it, and := writes it. Closures in the same worker share the cell.
- 06
Expressions & Iteration
Blocks, if, and match produce values. Tail calls reuse the current frame; for loops, ranges, comprehensions, and lazy Iter pipelines handle sequences.
- 07
Workers & Channels
Each worker has its own VM, heap, and globals. Transferable values cross by copy; typed bounded channels carry messages, and Join returns a Result.
- 08
Modules & Packages
Explicit imports, use declarations, exports, and module-qualified names. The project tool resolves local versioned packages, vendors sources, and writes a lockfile.
Quick Start
Install from the Marmot checkout on Windows or Linux, then use the project tool to create, check, format, run, and test a project.
python scripts/dev.py doctor python scripts/dev.py install --rebuild
marmot init hello-world cd hello-world marmot check src/Main.mmt marmot fmt marmot run src/Main.mmt marmot test
A complete .mmt file starts with an explicit module declaration. Top-level statements run in order. Search-path imports such as <IO>use the project's build plan or MARMOT_PATH; a path import is relative to its source file. Each language example below is a separate module.
module Main
import { <IO> }
IO::PrintLine("Hello, Marmot!");module FunctionsDemo // Simple function def square = fn(x: Int) -> Int => x * x; // Generic function def identity = fn<T>(value: T) -> T => value; // Higher-order function def apply = fn<T, R>(f: fn(T) -> R, value: T) -> R => f(value); def nine = apply(square, 3);
module TypesDemo
// Product types (records)
type Point = {
x: Float,
y: Float,
};
// Sum types
type Option<T> = None | Some(T);
type Result<T, E> = Ok(T) | Err(E);
def origin = Point(0.0, 0.0);
def moved = { origin with x = 3.0 }; // a new Point
def maybe = Option::Some(42);alias is transparent; type Meters = Int creates a distinct type over the same representation. Deriving generates helpers for supported shapes: Equatable, Hashable, and Transferablefor records and unions; Map, Bind, and Unwrapfor supported unions.
module DistinctTypesDemo alias UserId = Int; type Meters = Int; def user: UserId = 42; def distance: Meters = 12 as Meters; def raw: Int = distance as Int;
module DerivingDemo
import { <Equatable>, <Hashable> }
type Point = { x: Int, y: Int } deriving (Equatable, Hashable);
type Box<T> = Empty | Full(T) deriving (Map, Bind, Unwrap);
def same = Point(1, 2) == Point(1, 2);
def mapped = BoxMap(Box::Full(5), fn(n) => n + 1);
def value = BoxUnwrap(mapped, 0); // 6Pattern Matching
module MatchDemo
type Result<T, E> = Ok(T) | Err(E);
def describe = fn(result: Result<Int, Text>) -> Text =>
match result with
case Result::Ok(0) => "Success: zero"
case Result::Ok(value) => "Success: " ++ (value as Text)
case Result::Err(message) => "Error: " ++ message;Patterns can name record fields, bind an array's remaining elements with ..tail, match integer ranges, combine alternatives with |, or retain the whole value with as. Guards filter a case at runtime; guarded cases do not establish exhaustive coverage.
module ShapePatternsDemo
type Point = { x: Int, y: Int };
def axis = fn(point: Point) -> Text =>
match point with
case Point { x = 0, .. } as on_axis =>
"y=" ++ (on_axis.y as Text)
case Point { .. } => "off the y axis";
def first = fn(values: Array<Int>) -> Int =>
match values with
case [head, ..] => head
case [] => 0;module GuardPatternsDemo
def classify = fn(n: Int) -> Text =>
match n with
case -5..0 | 0..6 => "small"
case value if value > 100 => "large positive"
case _ => "other";
def small = classify(-3); // "small"
def large = classify(101); // "large positive"Typeclasses
Constrained generics with associated types and instance dispatch — preserving static guarantees.
module ShowDemo
class Show<T> {
show: fn(value: T) -> Text;
};
instance Show<Int> {
def show = fn(value: Int) -> Text => value as Text;
};
def display = fn<T>(value: T) -> Text where Show<T> => Show::show(value);
def message = display(42); // "42"module AssociatedTypesDemo
type Option<T> = None | Some(T);
// Next yields the item and the rest of the sequence, so an
// iterator is a value rather than something that advances in place.
class Iterable<Iter> {
type Item;
Next: fn(iter: Iter) -> Option<(Item, Iter)>;
};
def NextValue = fn<Iter>(iter: Iter) -> Option<(Iterable::Item<Iter>, Iter)>
where Iterable<Iter> => Iterable::Next(iter);Advanced
module PipeDemo
def double = fn(x: Int) -> Int => x * 2;
type Result<T, E> = Ok(T) | Err(E);
def transform = fn(value: Int) -> Result<Int, Text> =>
if value > 10
then Result::Ok(value + 1)
else Result::Err("too small");
def result =
5
|> double
|> fn(x) => { x + 1 }
|> transform
|> match with
case Result::Ok(value) => value
case Result::Err(_) => 0;module ClosuresDemo
def ApplyTwice = fn(f: fn(Int) -> Int, x: Int) -> Int => f(f(x));
// The lambda captures factor from the enclosing function.
def ScaleTwice = fn(factor: Int, values: Array<Int>) -> Array<Int> =>
[ApplyTwice(fn(x: Int) -> Int => x * factor, value) for value in values];
def scaled = ScaleTwice(3, [1, 2, 3]); // [9, 18, 27]Iter builds lazy sequence pipelines over Iterable sources. Stages run when a consumer such as ToArray or Fold asks for their elements.
module SequenceDemo
import { <Iter> }
def values = [1, 2, 3, 4, 5, 6];
def evens = values
|> Iter::OfArray
|> Iter::Filter(fn(n: Int) -> Bool => (n % 2) == 0)
|> Iter::Map(fn(n: Int) -> Int => n * 2)
|> Iter::Take(2)
|> Iter::ToArray; // [4, 8]References & Recursion
A def binds a name once. Changing state lives in a Ref<T>: aliases and closures within a worker see writes to the same cell. Reference creation, reading, and writing need no import; a write evaluates to Unit. A call in tail position reuses the current frame, including through blocks, if branches, and match arms.
module ReferencesDemo
def counter = ref 0;
def Next = fn() -> Int => {
counter := (*counter) + 1;
*counter
};
def first = Next(); // 1
def second = Next(); // 2
def current = *counter; // 2module RecursionDemo
def SumTo = fn(n: Int, total: Int) -> Int =>
if n == 0 then total else SumTo(n - 1, total + n);
def total = SumTo(1000, 0); // 500500
def squares = [n * n for n in 0..1..5]; // [0, 1, 4, 9, 16]Modules & Packages
An import makes exported names available through their module; usebrings selected names into local scope. Types retain their module identity. The marmot tool reads project.marmot and package.marmot, resolves versions from local package directories, vendors sources into packages/, and writes marmot.lock. The package workflow and dynamic native FFI are experimental; remote registry fetching is not implemented.
module Arithmetic
public export { Double }
def Double = fn(n: Int) -> Int => n * 2;module ModuleDemo
import { "./Arithmetic.mmt" }
use Arithmetic.{Double}
def qualified = Arithmetic::Double(5);
def local = Double(5);Workers & Channels
Each worker runs on an OS thread with its own heap and a snapshot of the spawning VM's globals. Arguments and closure captures cross by copy; a transferred Ref<T> becomes an independent cell. Channels retain their shared handles and carry Transferable values. Join returns Result<T, WorkerError>, with cancellation or failure in Err. Channel sends and receives block when full or empty; Cancel requests cooperative cancellation.
module Workers
import
{
<IO>,
<Concurrency>,
<Prelude.Result>
}
use Result.{Result}
def SumTo = fn(n: Int, total: Int) -> Int =>
if n == 0 then total else SumTo(n - 1, total + n);
// Spawn runs SumTo on its own worker; Join waits for the result.
def worker = (1000, 0) |> Concurrency::Spawn(SumTo);
def total =
match Concurrency::Join(worker) with
case Result::Ok(value) => value
case Result::Err(_) => 0;
// ParallelMap splits the array into chunks, one worker each.
def squares = Concurrency::ParallelMap([n for n in 0..1..8], fn(n: Int) -> Int => n * n, 4);
// Channels carry typed values between workers.
def channel : Channel<Int> = Concurrency::MakeChannel(4);
channel -> total;
def received = <- channel;
Concurrency::Close(channel);
IO::PrintLine("total=" ++ (total as Text) ++ " received=" ++ (received as Text) ++ " last square=" ++ (squares[7] as Text));Typed Prelude
MarmotPrelude wraps the runtime FFI surface in Marmot-facing modules. File and process APIs prefer typed Result and Option over sentinel returns.
module ConfigDemo
import
{
<IO>,
<System>,
<Prelude.Option>,
<Prelude.Result>
}
use Option.{Option}
use Result.{Result}
def config_text =
match IO::TryReadFile("app.conf") with
case Result::Ok(contents) => contents
case Result::Err(_) => "release=debug"
;
def cache_dir =
match System::TryGetEnv("MARMOT_CACHE_DIR") with
case Option::Some(path) => path
case Option::None() => ".marmot-cache"
;Standard Library
- 01
IO.mmtConsole output and typed file APIs: TryReadFile, TryWriteFile, TryAppendToFile, and binary-file helpers. - 02
System.mmtEnvironment lookup through Option; fallible directory and process operations through Result; platform and process information. - 03
DateTime.mmtLocalNow, UtcNow, FormatLocal, Unix milliseconds, and monotonic nanoseconds. - 04
TextUtil.mmtUTF-8 text helpers, splitting, trimming, replacement, and fallible ParseInt / ParseFloat conversions. - 05
ArrayUtil.mmtWithAppended, WithReplaced, Slice, and Reverse return new arrays. - 06
Iter.mmtLazy Map, Filter, and Take stages over Iterable sources; ToArray, Fold, and Count consume the pipeline. - 07
Collections/Map & SetPersistent hash-based collections. Updates return new collections; keys use Hashable and Equatable. - 08
Concurrency.mmtSpawn, Join, MakeChannel, Close, IsDone, and cooperative Cancel; ParallelMap is written in Marmot. - 09
Prelude/Option, Result, ListCore algebraic data types and helpers. Result uses Ok and Err; Option uses Some and None.
Architecture
The three tools have separate jobs. marmotc checks or compiles source and writes .mmc; marmotvm runs or disassembles it. marmot manages projects and drives both. Project runs build into target/ and reuse the result while their inputs are unchanged.
marmotc check src/Main.mmt marmotc build src/Main.mmt -o hello.mmc marmotvm run hello.mmc marmotvm disassemble hello.mmc marmotc check src/Main.mmt --emit-ir
- 01FrontendLexer → Module Manager → Parser → Type Checker → Static Analyzer
- 02LoweringChecked AST → MidoriIR, a typed SSA representation. Generics specialize and typeclass calls resolve to instance methods.
- 03OptimizerIR passes include inlining, contification, scalar replacement, constant propagation, global value numbering, and loop-invariant code motion. The central pass group repeats until unchanged, for at most eight rounds.
- 04BackendBytecode Backend → Linker → versioned .mmc program. Dev and Debug verify the IR after lowering and each pass; Release verifies before emission.
- 05Runtimemarmotvm loads and executes .mmc bytecode with a non-moving, generational bitmap mark-sweep collector. Each worker owns its heap, stack, globals, and collector while sharing read-only bytecode.
- 06Project Toolmarmot, written in Rust, resolves projects and packages and passes an explicit build plan to marmotc. marmotc compiles; marmotvm runs.