← Works

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.

01

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.

    ·
02

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.

Install from Source
python scripts/dev.py doctor
python scripts/dev.py install --rebuild
Project Workflow
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.

Hello World · src/Main.mmt
module Main

import { <IO> }

IO::PrintLine("Hello, Marmot!");
Functions
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);
Algebraic Data Types
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.

Aliases & Distinct Types
module DistinctTypesDemo

alias UserId = Int;
type Meters = Int;

def user: UserId = 42;
def distance: Meters = 12 as Meters;
def raw: Int = distance as Int;
Deriving
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);  // 6
03

Pattern Matching

Match Expression
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.

Records & Array Rest Patterns
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;
Ranges, Alternatives & Guards
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"
04

Typeclasses

Constrained generics with associated types and instance dispatch — preserving static guarantees.

Show Typeclass
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"
Associated Types
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);
05

Advanced

Pipe Into Match
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;
Closures
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.

Lazy Sequence Pipeline
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]
06

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.

Shared Cell in a Closure
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;  // 2
Tail Recursion & Comprehensions
module 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]
07

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.

Export · Arithmetic.mmt
module Arithmetic
public export { Double }

def Double = fn(n: Int) -> Int => n * 2;
Import & use · ModuleDemo.mmt
module ModuleDemo

import { "./Arithmetic.mmt" }
use Arithmetic.{Double}

def qualified = Arithmetic::Double(5);
def local = Double(5);
08

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.

Workers
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));
09

Typed Prelude

MarmotPrelude wraps the runtime FFI surface in Marmot-facing modules. File and process APIs prefer typed Result and Option over sentinel returns.

Typed IO & System
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"
    ;
10

Standard Library

  1. 01IO.mmtConsole output and typed file APIs: TryReadFile, TryWriteFile, TryAppendToFile, and binary-file helpers.
  2. 02System.mmtEnvironment lookup through Option; fallible directory and process operations through Result; platform and process information.
  3. 03DateTime.mmtLocalNow, UtcNow, FormatLocal, Unix milliseconds, and monotonic nanoseconds.
  4. 04TextUtil.mmtUTF-8 text helpers, splitting, trimming, replacement, and fallible ParseInt / ParseFloat conversions.
  5. 05ArrayUtil.mmtWithAppended, WithReplaced, Slice, and Reverse return new arrays.
  6. 06Iter.mmtLazy Map, Filter, and Take stages over Iterable sources; ToArray, Fold, and Count consume the pipeline.
  7. 07Collections/Map & SetPersistent hash-based collections. Updates return new collections; keys use Hashable and Equatable.
  8. 08Concurrency.mmtSpawn, Join, MakeChannel, Close, IsDone, and cooperative Cancel; ParallelMap is written in Marmot.
  9. 09Prelude/Option, Result, ListCore algebraic data types and helpers. Result uses Ok and Err; Option uses Some and None.
11

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.

Compiler & VM
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
  1. 01FrontendLexer → Module Manager → Parser → Type Checker → Static Analyzer
  2. 02LoweringChecked AST → MidoriIR, a typed SSA representation. Generics specialize and typeclass calls resolve to instance methods.
  3. 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.
  4. 04BackendBytecode Backend → Linker → versioned .mmc program. Dev and Debug verify the IR after lowering and each pass; Release verifies before emission.
  5. 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.
  6. 06Project Toolmarmot, written in Rust, resolves projects and packages and passes an explicit build plan to marmotc. marmotc compiles; marmotvm runs.
12

Get Started

View on GitHubDocumentation ↗Try the Playground →