Mac Language Reference

Mac (Meme as Code) is a programming language where memes are first-class citizens. It combines a general-purpose scripting language with a built-in rendering pipeline for creating memes and animated GIFs.

Quick Example

// Create a meme and save it
@two_panel "Writes code" "It works first try" => "miracle.png";

// Or use the full syntax with styles and effects
style bold_red { color: "#FF0000", fontWeight: "bold" }

@blank 720x720 bold_red {
    top: "Mac Language"
    bottom: "Meme as Code"
} |> sepia |> border(3) => "styled.png";

Language Features

FeatureDescription
Dynamic typingValues are strings, numbers, booleans, nil, arrays, or maps
First-class functionsFunctions are values, with closures, lambdas, and implicit returns
ClassesSingle inheritance, constructors, methods, fields
EnumsExhaustive sum types with match destructuring (Result, Option)
Pattern matchingmatch expr { Pattern -> result } with enum destructuring
String interpolation"Hello, {name}!" embeds expressions in strings
Destructuringvar [a, b] = expr and for (var [k, v] in pairs)
Immutable bindingsval x = 42 prevents reassignment
Pipe operatorvalue |> func for pipeline-style composition
Partial applicationpartial(fn, arg) creates pre-filled functions
Expression blocks{ statements; tail_expr } — last expression is the block's value
Meme literals@template "text" creates renderable meme objects
Effectsblur(5), sepia, grayscale — composable image transforms
Grid layoutgrid 2x2 { ... } for multi-panel compositions
GIF animationgif { ... } with per-frame timing, transitions, and easing curves
Save operatorexpr => "file.png" writes output to disk

Version

This reference documents Mac v0.8.0.

Types

Mac is dynamically typed. Every value belongs to one of the following types at runtime.

Scalar Types

TypeExampleDescription
number42, 3.14IEEE 754 double-precision floating point
string"hello"Double-quoted UTF-8 string
booltrue, falseBoolean
nilnilAbsence of a value

Numbers

All numbers are 64-bit floating point. There is no separate integer type.

var x = 42;
var pi = 3.14159;
var neg = -7;

Strings

Strings are delimited by double quotes.

var greeting = "Hello, World!";

String Interpolation

Any double-quoted string can embed expressions inside { and }. The expression is evaluated at runtime and its result is converted to a string and concatenated in place.

var name = "Mac";
print "Hello, {name}!";           // Hello, Mac!
print "2 + 2 = {2 + 2}";         // 2 + 2 = 4
print "{upper(name)} LANG";       // MAC LANG

To include a literal { in a string, escape it with a backslash:

print "literal \{brace}";         // literal {brace}

The + operator also auto-converts non-string values to strings when the other operand is a string:

print "count: " + 42;             // count: 42

String Templates

The @"path" syntax resolves a file path relative to the script's directory.

var img = @"assets/photo.png";

Collection Types

TypeExampleDescription
[T][1, 2, 3]Ordered, mutable array
{T}{"a": 1}Key-value map (string keys)

See Arrays and Maps for details.

Enum Types

TypeExampleDescription
enumenum Color { Red, Green, Blue }User-defined tagged union with optional data

Enum variants are accessed via EnumName.Variant. Data variants are constructed like function calls: Shape.Circle(5). See Enums for details.

Meme Types

TypeProduced byDescription
Meme@template "text"Renderable meme image
Gifgif { ... }Animated GIF (supports transitions)
RenderSurfaceEffects pipelineIn-memory pixel buffer

Type Checking

Use type() to inspect a value's type at runtime.

print type(42);          // number
print type("hi");        // string
print type(true);        // bool
print type(nil);         // nil
print type([1, 2]);      // array
print type({"a": 1});    // map

Variables

Declaration

Variables are declared with var and an optional initializer.

var x = 42;
var name = "Mac";
var uninitialized;        // value is nil

Variables must be declared before use. Referencing an undefined variable is a runtime error.

Immutable Bindings

Use val to declare a variable that cannot be reassigned.

val name = "Mac";
print name;                       // Mac
name = "other";                   // Runtime Error: Cannot reassign 'val' binding 'name'.

A val declaration must always have an initializer:

val x = 42;
val y = x + 1;                    // Fine — val doesn't freeze the value, just the binding
print y;                          // 43

Destructuring also works with val:

val [a, b] = [1, 2];
print a;                          // 1

Assignment

var count = 0;
count = count + 1;

Block Expressions

A block { } is an expression -- its value is the last expression in the block when written without a trailing semicolon. This lets you use blocks anywhere a value is expected:

val x = {
    val a = 1;
    val b = 2;
    a + b     // block value
};
print x;      // 3

Scoping

Variables are block-scoped. A variable declared inside { } is not visible outside it.

var outer = "visible";
{
    var inner = "hidden";
    print outer;          // visible
    print inner;          // hidden
}
// print inner;           // Runtime Error: Undefined variable 'inner'.

Inner scopes can read and assign to variables from enclosing scopes.

var x = 1;
{
    x = 2;                // assigns to outer x
}
print x;                  // 2

Array Destructuring

Unpack array elements into individual variables using a destructuring pattern.

var [a, b, c] = [1, 2, 3];
print a;                  // 1
print b;                  // 2
print c;                  // 3

This works with any expression that evaluates to an array:

var [first, second] = split("hello-world", "-");
print first;              // hello
print second;             // world

Destructuring also works in for-in loops — see Control Flow.

Shadowing

A new var declaration in an inner scope creates a separate variable that shadows the outer one.

var x = "outer";
{
    var x = "inner";
    print x;              // inner
}
print x;                  // outer

Operators

Arithmetic

OperatorDescriptionExample
+Addition / string concatenation2 + 35, "a" + "b""ab"
-Subtraction / negation5 - 32, -x
*Multiplication4 * 312
/Division10 / 42.5
%Modulo7 % 31

Comparison

OperatorDescription
==Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal

Comparisons return true or false.

Logical

OperatorDescription
andLogical AND (short-circuit)
orLogical OR (short-circuit)
!Logical NOT
print true and false;     // false
print true or false;      // true
print !true;              // false

Pipe

The pipe operator passes the left-hand value as the first argument to the right-hand function.

value |> func
// equivalent to: func(value)

Pipes chain naturally for multi-step processing:

@blank "Hello"
    |> sepia
    |> blur(3)
    |> border(2)
    => "processed.png";

Compose

The compose operator creates a new function from two existing functions.

var transform = sepia >> blur(3) >> border(2);
@blank "Hello" |> transform => "composed.png";

Save

The fat arrow => saves a meme or GIF to a file.

@blank "Hello" => "hello.png";

See Saving Output for details.

Precedence

From lowest to highest:

PrecedenceOperators
1=> (save)
2|> (pipe)
3>> (compose)
4or
5and
6== !=
7< <= > >=
8+ -
9* / %
10! - (unary)
11. () [] (call, access)

Control Flow

if / else

if (condition) {
    // then branch
} else {
    // else branch
}

The else branch is optional. Parentheses around the condition are required.

var x = 10;
if (x > 5) {
    print "big";
} else {
    print "small";
}
// Output: big

while

while (condition) {
    // body
}
var i = 0;
while (i < 3) {
    print i;
    i = i + 1;
}
// Output: 0  1  2

for

The C-style for loop with initializer, condition, and increment:

for (var i = 0; i < 5; i = i + 1) {
    print i;
}

for-in

Iterate over arrays or maps:

var items = ["a", "b", "c"];
for (var item in items) {
    print item;
}
// Output: a  b  c

for-in Destructuring

When iterating over arrays of arrays, you can destructure each element directly in the loop variable:

for (var [i, val] in enumerate(["a", "b", "c"])) {
    print "{i}: {val}";
}
// Output: 0: a  1: b  2: c
for (var [name, score] in zip(["Alice", "Bob"], [95, 87])) {
    print "{name}: {score}";
}
// Output: Alice: 95  Bob: 87

This is equivalent to accessing each element by index inside the loop body but more concise.

match

A match expression evaluates a value against a series of patterns and returns the result of the matching arm. It can be used anywhere an expression is valid.

var label = match score {
    100 -> "Perfect"
    0 -> "Zero"
    _ -> "Other"
};

Each arm is a pattern followed by -> and a result expression. Arms can use block expressions to compute their result:

val result = match value {
    Result.Ok(v) -> {
        val doubled = v * 2;
        doubled + 1
    }
    Result.Error(e) -> -1
};

The last expression in a block arm (without a trailing semicolon) becomes the arm's value.

The _ wildcard matches any value. If no arm matches and there is no wildcard, the result is nil.

var x = match 42 {
    0 -> "zero"
    42 -> "forty-two"
    _ -> "other"
};
print x;
// Output: forty-two

Patterns are compared using ==. Any expression can be used as a pattern:

var n = 5;
var result = match n {
    2 + 3 -> "five"
    _ -> "not five"
};
// Output: five

Match works with any value type -- numbers, strings, booleans, and function return values:

var icon = match type(value) {
    "number" -> "#"
    "string" -> "abc"
    _ -> "?"
};

Since match is an expression, it can be used inline:

print match 1 + 1 {
    2 -> "correct"
    _ -> "wrong"
};
// Output: correct

Enum Destructuring

Match can destructure enum variants and bind their fields to local variables:

enum Result { Ok(value), Error(message) }

val msg = match Result.Ok(42) {
    Result.Ok(v) -> "got {v}"
    Result.Error(e) -> "err: {e}"
};
// Output: got 42

Simple enum variants match by identity:

enum Color { Red, Green, Blue }
val label = match Color.Red {
    Color.Red -> "danger"
    Color.Green -> "go"
    _ -> "other"
};
// Output: danger

break

Exit the innermost loop immediately.

for (var i = 0; i < 10; i = i + 1) {
    if (i == 3) break;
    print i;
}
// Output: 0  1  2

continue

Skip to the next iteration of the innermost loop.

for (var i = 0; i < 5; i = i + 1) {
    if (i == 2) continue;
    print i;
}
// Output: 0  1  3  4

Functions

Declaration

fun name(param1, param2) {
    // body
    return value;
}

Functions are first-class values. They can be stored in variables, passed as arguments, and returned from other functions.

fun add(a, b) {
    return a + b;
}
print add(2, 3);          // 5

Return

The return statement exits the function and produces a value. If omitted, the function returns nil.

fun greet(name) {
    return "Hello, " + name + "!";
}
print greet("Mac");       // Hello, Mac!

Implicit Return

The last expression in a function body, when written without a trailing semicolon, becomes the return value of the function.

fun add(a, b) {
    a + b
}
// equivalent to: fun add(a, b) { return a + b; }

A trailing semicolon suppresses the implicit return -- the function returns nil instead:

fun f() { 42; }
print f();                // nil

Explicit return statements still work as before and take precedence.

Closures

Functions capture variables from their enclosing scope.

fun makeCounter() {
    var count = 0;
    fun increment() {
        count = count + 1;
        return count;
    }
    return increment;
}

var counter = makeCounter();
print counter();              // 1
print counter();              // 2

Functions as Values

fun apply(f, x) {
    return f(x);
}

fun double(n) { return n * 2; }

print apply(double, 5);      // 10

Recursion

Functions can call themselves.

fun factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
print factorial(5);           // 120

See Also

Lambdas

Lambdas are anonymous functions, useful for short callbacks and inline transforms.

Syntax

// No parameters
fun() { body }

// With parameters
fun(a, b) { body }

// Arrow shorthand (single expression, implicit return)
(params) -> expression

Examples

var double = fun(x) { return x * 2; };
print double(5);                  // 10

// Arrow form
var triple = (x) -> x * 3;
print triple(5);                  // 15

With Higher-Order Functions

Lambdas are commonly used with map, filter, and reduce:

var nums = [1, 2, 3, 4, 5];

var doubled = map(nums, (x) -> x * 2);
print doubled;                    // [2, 4, 6, 8, 10]

var evens = filter(nums, (x) -> x % 2 == 0);
print evens;                      // [2, 4]

var sum = reduce(nums, (acc, x) -> acc + x, 0);
print sum;                        // 15

With Pipes

Lambdas combine naturally with the pipe operator:

[1, 2, 3, 4, 5]
    |> filter((x) -> x > 2)
    |> map((x) -> x * 10)
    |> reduce((a, b) -> a + b, 0);
// Result: 120

Closures

Lambdas capture variables from their enclosing scope, just like named functions.

var factor = 3;
var scale = (x) -> x * factor;
print scale(10);                  // 30

Meme Factories

Arrow lambdas returning meme expressions create reusable meme generators:

var meme_factory = (text) -> @blank text;
meme_factory("Hello!") => "hello.png";

See Also

Classes

Mac supports single-inheritance classes with constructors, methods, and fields.

Declaration

class Name {
    init(params) {
        this.field = value;
    }
    method(params) {
        return this.field;
    }
}

Constructor

The init method is called automatically when a class is invoked as a function.

class Point {
    init(x, y) {
        this.x = x;
        this.y = y;
    }
}

var p = Point(3, 4);
print p.x;                   // 3

Methods

Methods are functions that have access to this.

class Circle {
    init(radius) {
        this.radius = radius;
    }
    area() {
        return 3.14159 * this.radius * this.radius;
    }
}

var c = Circle(5);
print c.area();               // 78.53975

Fields

Fields are created by assigning to this.name inside any method. There are no field declarations outside methods.

class Config {
    init() {
        this.debug = false;
        this.verbose = true;
    }
}

Inheritance

Use < to extend a superclass.

class Animal {
    init(name) { this.name = name; }
    speak() { return this.name + " makes a sound"; }
}

class Dog < Animal {
    init(name) { super.init(name); }
    speak() { return this.name + " barks"; }
}

var d = Dog("Rex");
print d.speak();              // Rex barks

super

The super keyword calls the superclass version of a method.

class Cat < Animal {
    init(name) { super.init(name); }
    describe() {
        return super.speak() + " (it's a cat)";
    }
}

Operator Overloading

Methods named __add__, __mul__, __eq__ override +, *, == respectively.

class Vec {
    init(x, y) { this.x = x; this.y = y; }
    __add__(other) {
        return Vec(this.x + other.x, this.y + other.y);
    }
}

var a = Vec(1, 2);
var b = Vec(3, 4);
var c = a + b;
print c.x;                    // 4

Visibility

Fields and methods prefixed with _ are treated as internal by the analyzer (LSP warnings for external access).

class Secret {
    init() {
        this._hidden = 42;    // internal
        this.visible = true;  // public
    }
    _helper() { }             // internal
    api() { }                 // public
}

See Also

Arrays

Ordered, mutable collections of values.

Literal

var empty = [];
var nums = [1, 2, 3];
var mixed = ["hello", 42, true, nil];

Indexing

Zero-based. Use [] to read or write elements.

var a = [10, 20, 30];
print a[0];                   // 10
a[1] = 99;
print a;                      // [10, 99, 30]

Length

print len([1, 2, 3]);         // 3

Mutation

var a = [1, 2];
push(a, 3);                   // [1, 2, 3]
var last = pop(a);             // last = 3, a = [1, 2]

Iteration

var colors = ["red", "green", "blue"];
for color in colors {
    print color;
}

Or with each:

each(colors, (c) -> print(c));

Functional Operations

var nums = [1, 2, 3, 4, 5];

map(nums, (x) -> x * 2);              // [2, 4, 6, 8, 10]
filter(nums, (x) -> x > 3);           // [4, 5]
reduce(nums, (acc, x) -> acc + x, 0); // 15
find(nums, (x) -> x > 3);             // 4
any(nums, (x) -> x > 4);              // true
all(nums, (x) -> x > 0);              // true

Transformation

reverse([1, 2, 3]);                    // [3, 2, 1]
sort([3, 1, 2]);                       // [1, 2, 3]
flatten([[1, 2], [3, 4]]);             // [1, 2, 3, 4]
take([1, 2, 3, 4], 2);                 // [1, 2]
drop([1, 2, 3, 4], 2);                 // [3, 4]
zip([1, 2], ["a", "b"]);              // [[1, "a"], [2, "b"]]
enumerate(["a", "b"]);                 // [[0, "a"], [1, "b"]]
join(["a", "b", "c"], ", ");           // "a, b, c"

Nested Arrays

var matrix = [[1, 2], [3, 4]];
print matrix[0][1];                    // 2

See Also

Maps

Key-value collections with string keys. Insertion order is preserved.

Literal

var empty = {};
var user = {"name": "Alice", "age": 30};

Keys must be strings. Values can be any type.

Access

var m = {"x": 10, "y": 20};
print m["x"];                 // 10
m["z"] = 30;
print m;                      // {x: 10, y: 20, z: 30}

Iteration

var config = {"debug": true, "verbose": false};
for entry in config {
    print entry;
}

Nested Maps

var data = {
    "user": {
        "name": "Mac",
        "scores": [10, 20, 30]
    }
};
print data["user"]["name"];   // Mac

See Also

Enums

Enums define a closed set of named variants. Variants can optionally carry data.

Declaration

enum Color { Red, Green, Blue }

This creates an enum type Color with three simple variants. Access variants via EnumName.Variant:

var c = Color.Red;
print c;           // Red
print type(c);     // Color

Data Variants

Variants can carry named fields:

enum Shape { Circle(radius), Rect(width, height), Point }

Data variants are constructed by calling them like functions:

var circle = Shape.Circle(5);
var rect = Shape.Rect(3, 4);
print circle;      // Circle(5)
print rect;        // Rect(3, 4)
print Shape.Point; // Point

Simple variants (no fields) are values, not constructors. Data variants are callable constructors.

Equality

Enum values support == and !=. Two enum values are equal if they have the same variant and the same field values:

print Color.Red == Color.Red;    // true
print Color.Red == Color.Blue;   // false

Match Destructuring

The match expression can destructure enum variants and bind their fields to variables:

enum Result { Ok(value), Error(message) }

val msg = match Result.Ok(42) {
    Result.Ok(v) -> "got {v}"
    Result.Error(e) -> "err: {e}"
};
print msg;         // got 42

Each match arm uses EnumName.Variant(bindings) syntax. The bound variables are available in the result expression.

The wildcard _ matches any value:

val x = match color {
    Color.Red -> "danger"
    _ -> "other"
};

If no arm matches and there is no wildcard, the result is nil.

Built-in Enums

The prelude defines two commonly-used enum types:

Result

enum Result { Ok(value), Error(message) }

Use Result for operations that can succeed or fail:

fun divide(a, b) {
    if (b == 0) return Result.Error("division by zero");
    return Result.Ok(a / b);
}

val answer = match divide(10, 2) {
    Result.Ok(v) -> "result: {v}"
    Result.Error(e) -> "error: {e}"
};
print answer;      // result: 5

Option

enum Option { Some(value), None }

Use Option for values that may or may not exist:

fun find_first(arr, pred) {
    for (var item in arr) {
        if (pred(item)) return Option.Some(item);
    }
    return Option.None;
}

val found = match find_first([1, 2, 3], (x) -> x > 2) {
    Option.Some(v) -> "found: {v}"
    Option.None -> "not found"
};
print found;       // found: 3

Meme Literals

Meme literals create renderable meme images using the @ prefix.

Syntax

// Quick form — template name and a single text string
@template "text"

// Full form — with dimensions, style, and named text slots
@template [WIDTHxHEIGHT] [styleName] {
    top: "Top text"
    bottom: "Bottom text"
    center: "Center text"
}

Quick Form

A template name followed by a string creates a meme with that text as the top line.

@blank "Hello, World!";
@two_panel "Before coffee" "After coffee";
@bottom_text "When the code works";

Multi-string memes fill top, bottom, center in order depending on the template.

Full Form

Use { } for explicit text slot assignment.

@blank {
    top: "Mac Language"
    bottom: "Meme as Code"
}

Dimensions

Specify output size with WIDTHxHEIGHT before the text block.

@blank 1080x1080 {
    top: "HD Meme"
}

Default dimensions depend on the template (typically 720x720).

Custom Images

Use a file path instead of a template name.

@"photos/cat.jpg" "Caption text";

String template paths (@"path") resolve relative to the script's directory.

Saving

Memes are saved with the => operator or save() function.

@blank "Hello" => "hello.png";

var m = @two_panel "Top" "Bottom";
save(m, "meme.jpg");

Output files are written to ~/mac/output/ by default. Explicit relative and absolute save paths are respected unless MAC_OUTPUT_DIR is set by the host.

With Styles

Apply a named style for text customization.

style red_bold {
    color: "#FF0000"
    fontWeight: "bold"
}

@blank 720x720 red_bold {
    top: "Styled Text"
}

With Effects

Pipe memes through effect functions.

@blank "Vintage" |> sepia |> vignette => "vintage.png";

With Positioned Text

Use x and y for absolute text positioning.

@blank 720x720 {
    text: "Anywhere" x: 100 y: 200
}

See Also

Styles

Style blocks define reusable text rendering configurations.

Syntax

style name {
    property: value
    property: value
}

Properties

PropertyTypeDefaultDescription
colorhex string"#FFFFFF"Text color (#RRGGBB or #RRGGBBAA)
outlinenumber3Outline stroke width in pixels
outlineWidthnumber3Alias for outline
outlineColorhex string"#000000"Outline color
shadownumber0Drop shadow offset in pixels
shadowColorhex string"#000000"Shadow color
fontSizenumber or string"md"Font size: pixels or "sm", "md", "lg", "xlg"
fontWeightstring"bold""normal" or "bold"
textTransformstring"uppercase""none" or "uppercase"
backgroundhex stringnoneBackground fill color (with alpha)

Example

style impact {
    color: "#FFFFFF"
    outline: 4
    outlineColor: "#000000"
    fontWeight: "bold"
    textTransform: "uppercase"
}

style caption {
    color: "#000000"
    outline: 0
    fontWeight: "normal"
    textTransform: "none"
    background: "#FFFFFFCC"
}

@blank 720x720 impact { top: "Impact Style" } => "impact.png";
@blank 720x720 caption { bottom: "Caption Style" } => "caption.png";

Font Sizes

PresetApproximate Size
"sm"Small
"md"Medium (default)
"lg"Large
"xlg"Extra large

Or specify exact pixel size:

style small_text { fontSize: 24 }
style huge_text { fontSize: 72 }

Background Color

The background property fills the area behind text. Use alpha for transparency.

style subtitle {
    color: "#FFFFFF"
    background: "#00000099"
    outline: 0
    textTransform: "none"
}

Inline Style

Styles can also be applied using keyword modifiers after the template:

@blank cinematic { top: "Movie Style" }

Built-in style keywords:

KeywordEffect
impactWhite text, black outline, bold, uppercase
cinematicLetter-boxed look
shoutLarge bold uppercase
whisperSmall, normal weight
panicRed text, heavy outline

See Also

Effects

Effects are image transforms applied to memes via the pipe operator or composition.

Usage

// Pipe a meme through an effect
@blank "Hello" |> sepia => "sepia.png";

// Chain multiple effects
@blank "Vintage" |> sepia |> vignette |> blur(2) => "vintage.png";

// Compose effects into a reusable transform
var retro = sepia >> vignette >> noise(0.05);
@blank "Retro" |> retro => "retro.png";

Non-Parameterized Effects

These take a meme directly and return a transformed meme.

EffectDescription
grayscaleConvert to grayscale
sepiaWarm sepia tone
invertInvert all colors
sharpenSharpen edges
vignetteDarken edges
@blank "Test" |> grayscale => "gray.png";

Parameterized Effects

These take a numeric argument and return a function that transforms a meme.

EffectParameterDescription
blur(radius)1-20Gaussian blur
pixelate(blockSize)2-50Pixelation
noise(amount)0.0-1.0Random noise overlay
saturate(factor)0.0-5.0Color saturation (1.0 = normal)
contrast(factor)0.0-5.0Contrast (1.0 = normal)
brightness(factor)0.0-3.0Brightness (1.0 = normal)
hueShift(degrees)0-360Rotate hue
glow(radius)1-20Bloom/glow effect
posterize(levels)2-32Reduce color levels
chromatic(offset)1-20RGB channel displacement
threshold(level)0-255Black/white binarization
tint(hexColor)hex stringColor tint overlay
jpeg(quality)1-100JPEG compression artifacts
@blank "Blurry" |> blur(5) => "blurred.png";
@blank "Warm" |> tint("#FF880044") => "tinted.png";

Named Effects

Define reusable effect compositions with the effect keyword.

effect deepfry = saturate(3.0) >> contrast(2.0) >> jpeg(10) >> noise(0.1);
effect lofi = grayscale >> noise(0.08) >> vignette;

@blank "Deep Fried" |> deepfry => "fried.png";
@blank "Lo-Fi" |> lofi => "lofi.png";

Effect Composition

Use >> to compose effects into a pipeline.

var dramatic = contrast(1.5) >> saturate(1.3) >> vignette;
var subtle = blur(1) >> brightness(1.1);

// Composed effects can be further composed
var full = dramatic >> subtle;

See Also

Grid Layout

Grid blocks compose multiple memes into a single image arranged in a grid.

Syntax

grid COLSxROWS {
    entry1
    entry2
    ...
}

Entries are meme expressions. The grid fills left-to-right, top-to-bottom.

Example

grid 2x2 {
    @blank "Top Left"
    @blank "Top Right"
    @blank "Bottom Left"
    @blank "Bottom Right"
} => "quad.png";

With Effects

Each entry can have its own effects:

grid 2x1 {
    @blank "Normal"
    @blank "Sepia" |> sepia
} => "comparison.png";

The entire grid can also be piped through effects:

grid 2x2 {
    @blank "A"
    @blank "B"
    @blank "C"
    @blank "D"
} |> border(3) => "bordered_grid.png";

With Padding and Borders

grid 2x2 {
    @blank "1"
    @blank "2"
    @blank "3"
    @blank "4"
} |> pad(10) |> border(2) => "framed.png";

Nested Grids

Grids can contain other grids:

grid 1x2 {
    grid 2x1 {
        @blank "A"
        @blank "B"
    }
    @blank "Footer"
} => "nested.png";

Composition Type

grid produces a frame type (Meme), which means it can be used inside:

  • Other grids
  • GIF frames
  • Effect pipelines

But it cannot directly contain sequence types (Gif).

See Also

GIF Animation

GIF blocks create animated GIFs from a sequence of frames with individual timing and optional transitions between them.

Syntax

gif {
    frame1 : duration
    frame2 : duration
    ...
}

Duration is specified in milliseconds (ms) or seconds (s).

Example

gif {
    @blank "Frame 1" : 500ms
    @blank "Frame 2" : 500ms
    @blank "Frame 3" : 1s
} => "animation.gif";

Duration Units

UnitExampleDescription
ms500msMilliseconds
s1.5sSeconds (converted to ms)

Transitions

Use --- separators between frames to add transitions. The syntax is:

gif {
    frame1 : hold_duration
    --- transition_type duration ---
    frame2 : hold_duration
    ...
}

Example

gif {
    @blank "Scene 1" : 2s
    --- crossfade 500ms ---
    @blank "Scene 2" : 2s
    --- fadeBlack 300ms ---
    @blank "Scene 3" : 2s
} => "movie.gif";

Transition Types

TransitionDescription
crossfadeBlend between frames
slideLeftNew frame slides in from right
slideRightNew frame slides in from left
slideUpNew frame slides in from bottom
slideDownNew frame slides in from top
wipeHorizontal wipe reveal
fadeBlackFade through black
zoomZoom into new frame

Easing Curves

Add an easing curve after the transition duration:

gif {
    @blank "Start" : 1s
    --- crossfade 500ms easeInOut ---
    @blank "End" : 1s
} => "eased.gif";
EasingDescription
linearConstant speed (default)
easeInAccelerate from rest
easeOutDecelerate to rest
easeInOutAccelerate then decelerate
bounceBouncing effect at end

Loop-Back Transition

The last --- transition applies when the GIF loops back from the final frame to the first, creating seamless loops.

gif {
    @blank "A" : 1s
    --- slideLeft 300ms ---
    @blank "B" : 1s
    --- slideLeft 300ms ---
    @blank "C" : 1s
    --- crossfade 500ms ---
} => "seamless.gif";
// The crossfade at the end transitions C back to A on loop

Mixed Mode

Transitions are optional between any pair of frames. Frames without a --- separator simply cut to the next frame with no transition:

gif {
    @blank "Intro" : 1s
    --- crossfade 500ms ---
    @blank "Main" : 2s
    @blank "Quick Cut" : 500ms
    --- fadeBlack 300ms ---
    @blank "Outro" : 1s
} => "mixed.gif";

With Effects

Each frame can be processed independently:

gif {
    @blank "Normal" : 400ms
    @blank "Sepia" |> sepia : 400ms
    @blank "Inverted" |> invert : 400ms
} => "effects.gif";

Frames with transitions can also have effects:

gif {
    @blank "Day" : 2s
    --- crossfade 500ms ---
    @blank "Night" |> tint("#000044AA") |> vignette : 2s
} => "day_night.gif";

With Grids

Grid layouts can be used as GIF frames:

gif {
    grid 2x1 { @blank "A" @blank "B" } : 500ms
    --- wipe 300ms ---
    grid 2x1 { @blank "C" @blank "D" } : 500ms
} => "grid_anim.gif";

Programmatic GIFs

Use the animate() function to create GIFs from arrays:

var frames = [
    @blank "One",
    @blank "Two",
    @blank "Three"
];
animate(frames, 500) => "uniform.gif";

Looping

GIFs loop infinitely by default.

Composition Type

gif produces a sequence type (Gif). Sequence types:

  • Can be saved directly
  • Cannot be placed inside grids or other frames
  • Cannot be nested inside other gifs

See Also

Saving Output

Save Operator

The => operator saves a meme or GIF to a file.

expression => "filename.ext"
@blank "Hello" => "hello.png";
@blank "Photo" => "photo.jpg";
gif { @blank "A" : 500ms @blank "B" : 500ms } => "anim.gif";

The save operator returns true on success.

save() Function

Equivalent to => but as a function call.

var m = @blank "Hello";
save(m, "hello.png");

Output Paths

When MAC_OUTPUT_DIR is not set, Mac resolves save paths like this:

InputOutput path
"hello.png"~/mac/output/hello.png
"sub/hello.png"./sub/hello.png (relative to cwd)
"/tmp/hello.png"/tmp/hello.png (absolute)

Any missing parent directories are created automatically on save.

MAC_OUTPUT_DIR

Set MAC_OUTPUT_DIR to force all saves into a specific directory:

MAC_OUTPUT_DIR=/tmp/mac-out mac script.mac

When this override is set, Mac keeps only the filename portion of the requested path:

Requested pathActual output path
"hello.png"/tmp/mac-out/hello.png
"sub/hello.png"/tmp/mac-out/hello.png
"/tmp/hello.png"/tmp/mac-out/hello.png

This is mainly useful for hosts like playgrounds, editors, and web apps that need all generated files to stay in one managed directory.

Supported Formats

ExtensionFormat
.pngPNG (lossless, default)
.jpg / .jpegJPEG (lossy)
.gifGIF (animated or static)

Confirmation

On every successful save, the actual absolute output path is printed to stderr:

  Saved /Users/you/mac/output/hello.png

This appears in REPL, file execution, and piped modes without interfering with stdout.

See Also

Native Functions

Built-in functions available in every Mac program without imports.

Time & Introspection

FunctionSignatureReturnsDescription
clockclock()numberWall-clock time in seconds
typetype(value)stringRuntime type name

String Operations

FunctionSignatureReturnsDescription
lenlen(str)numberString length (also works on arrays)
substrsubstr(str, start, length)stringExtract substring
splitsplit(str, delimiter)[string]Split into array
joinjoin(array, separator)stringJoin array into string
upperupper(str)stringConvert to uppercase
lowerlower(str)stringConvert to lowercase
trimtrim(str)stringRemove surrounding whitespace
replacereplace(str, from, to)stringReplace all occurrences

Math

FunctionSignatureReturnsDescription
sqrtsqrt(n)numberSquare root
absabs(n)numberAbsolute value
powpow(base, exp)numberExponentiation
floorfloor(n)numberRound down
ceilceil(n)numberRound up

Array Mutation

FunctionSignatureReturnsDescription
pushpush(array, value)nilAppend element (mutates)
poppop(array)TRemove and return last element

Array Transforms

FunctionSignatureReturnsDescription
mapmap(array, fn)[T]Transform each element
filterfilter(array, fn)[T]Keep matching elements
reducereduce(array, fn, init)TFold with accumulator
findfind(array, fn)TFirst matching element
flatMapflatMap(array, fn)[T]Map and flatten one level
flattenflatten(array)[T]Flatten one nesting level
reversereverse(array)[T]Reversed copy
sortsort(array)[T]Sorted copy
sortsort(array, cmp)[T]Sort with comparator
taketake(array, n)[T]First n elements
dropdrop(array, n)[T]Skip first n elements
zipzip(a, b)[[A, B]]Pair elements from two arrays
enumerateenumerate(array)[[number, T]]Index-value pairs
takeWhiletakeWhile(array, fn)[T]Take elements while predicate is true
dropWhiledropWhile(array, fn)[T]Drop elements while predicate is true
partitionpartition(array, fn)[[T], [T]]Split by predicate
groupBygroupBy(array, fn){string: [T]}Group by key function
uniqueunique(array)[T]Remove duplicates
chunkchunk(array, n)[[T]]Split into groups of n
scanscan(array, fn, init)[T]Reduce keeping intermediates
eacheach(array, fn)nilSide-effect iteration

Array Predicates

FunctionSignatureReturnsDescription
anyany(array, fn)boolAny element matches
allall(array, fn)boolAll elements match

Generators

FunctionSignatureReturnsDescription
rangerange(end)[number][0, 1, ..., end-1]
rangerange(start, end)[number][start, ..., end-1]
rangerange(start, end, step)[number]With custom step

Functional Utilities

FunctionSignatureReturnsDescription
partialpartial(fn, ...args)functionBind arguments to a function, returning a new function with fewer parameters

User Input

FunctionSignatureReturnsDescription
inputinput(prompt)stringRead line from stdin

Image Effects

See Effects for the full effect reference.

Parameterized (return a meme transform function):

blur(r), pixelate(s), noise(a), saturate(f), contrast(f), brightness(f), hueShift(d), glow(r), posterize(l), chromatic(o), threshold(l), tint(hex), jpeg(q)

Direct (take a meme, return a meme):

grayscale, sepia, invert, sharpen, vignette

Layout & Composition

FunctionSignatureReturnsDescription
besidebeside(left, right)MemeSide-by-side
stackstack(top, bottom)MemeVertical stack
gridgrid(cols, memes)MemeGrid arrangement
padpad(meme, px)MemeAdd white padding
borderborder(meme, px)MemeAdd black border

Animation

FunctionSignatureReturnsDescription
animateanimate(memes, ms)GifCreate GIF with uniform frame duration
toGridtoGrid(memes, cols, rows)MemeRender array to grid

I/O

FunctionSignatureReturnsDescription
savesave(target, path)boolSave meme/GIF to file

Prelude Classes

Classes defined in stdlib/prelude.mac, automatically loaded at startup.

Size

Pixel dimensions with arithmetic operators.

var s = Size(720, 480);
print s.width;                // 720
print s.height;               // 480

var doubled = s * 2;          // Size(1440, 960)
var combined = s + Size(100, 0); // Size(820, 480)
print s == Size(720, 480);    // true
MethodReturnsDescription
init(w, h)SizeConstructor
__add__(other)SizeAdd two sizes
__mul__(n)SizeMultiply by scalar
__eq__(other)boolEquality check

Duration

Time duration in milliseconds with arithmetic.

var d = Duration(500);
print d.ms;                   // 500

var longer = d * 3;           // Duration(1500)
var total = d + Duration(200); // Duration(700)
MethodReturnsDescription
init(ms)DurationConstructor
__add__(other)DurationAdd durations
__mul__(n)DurationMultiply by scalar
__eq__(other)boolEquality check

Meme

Builder-pattern meme construction (alternative to meme literals).

var m = Meme(Template("blank"));
m.text(Position("top"), "Hello");
m.text(Position("bottom"), "World");
m.save("png", "hello.png");
MethodReturnsDescription
init(template)MemeConstructor with Template
text(position, str)MemeAdd text at position (chainable)
resize(size)MemeCreate resized copy
save(format, path)nilSave to file

Gif

Animated GIF builder (alternative to gif { } blocks). Supports transitions via _tl for animated transitions between frames.

var g = Gif();
g.frame(@blank "One", Duration(500));
g.frame(@blank "Two", Duration(500));
g.save("output.gif");
MethodReturnsDescription
init()GifConstructor
frame(meme, duration)GifAdd frame (chainable)
transition(type, duration)GifAdd transition between frames (chainable)
save(path)nilSave as animated GIF
__add__(frame)GifAdd Frame object

Helper Classes

Position

Text position constants.

var top = Position("top");
var bottom = Position("bottom");
var center = Position("center");

Format

Output format constants.

var png = Format("png");
var jpg = Format("jpg");
var gif = Format("gif");

Template

Meme template reference.

var t = Template("two_panel");
var custom = Template("path/to/image.png");

See Also

Templates

Built-in meme templates available by name with the @ prefix.

Built-in Templates

TemplateDescriptionText Slots
blankWhite canvastop, bottom, center
darkDark/black canvastop, bottom, center
two_panelTwo-panel vertical splittop, bottom
three_panelThree-panel verticaltop, center, bottom
four_panel2x2 grid(4 entries)
bottom_textImage with bottom captionbottom
caption_barWhite bar caption above imagetop, bottom
wideWide aspect ratio canvastop, bottom, center
tallTall aspect ratio canvastop, bottom, center
squareSquare canvastop, bottom, center

Usage

@blank "Hello World";
@two_panel "Expectation" "Reality";
@four_panel "A" "B" "C" "D";

Custom Templates

Use a file path to any image as a template:

@"photos/cat.jpg" "Caption";
@"assets/background.png" { top: "Title" bottom: "Subtitle" }

String template paths resolve relative to the script's directory.

Meme Templates (assets)

Additional templates from assets/templates/meme/ are available with the meme. prefix:

@meme.shrek_smirk "When the code compiles";
@meme.kid_crying "Production is down";

Available meme templates depend on the installed asset pack.

See Also

Installation

Quick Install

curl -fsSL https://raw.githubusercontent.com/jona62/mac/main/install.sh | bash

This detects your platform, downloads the latest release binary, and installs to ~/.mac/ with a symlink at ~/.local/bin/mac.

Supported Platforms

PlatformArchitecture
macOSarm64 (Apple Silicon)
macOSx86_64 (Intel)
Linuxx86_64

Updating

Run the install script again. It detects existing installations and updates in place.

Mac also checks for updates on startup (once per 24 hours) and prints a message if a newer version is available.

Build from Source

git clone https://github.com/jona62/mac.git
cd mac
cmake -S . -B build
cmake --build build
./build/mac --version

Requires CMake 3.20+ and a C++23 compiler.

Uninstall

mac --uninstall

Removes ~/.mac/ (binary, assets, stdlib), ~/.local/bin/mac (symlink), and ~/.mac_history (REPL history). Prompts before deleting ~/mac/output/ (your generated files).

File Locations

PathContents
~/.mac/Binary, assets, stdlib
~/.local/bin/macSymlink to binary
~/.mac_historyREPL command history
~/.mac/update_checkUpdate check cache
~/mac/output/Default generated output for bare filenames

REPL

The interactive Read-Eval-Print Loop starts when mac is run with no arguments.

$ mac
|> print "Hello, Mac!";
Hello, Mac!
|>

Prompt

PromptMeaning
|>Ready for input
..Continuation (inside { }, [ ], or ( ))

Multi-Line Input

Open braces, brackets, or parentheses automatically enter multi-line mode. The REPL waits for all delimiters to close before executing.

|> var greet = (name) -> {
..     return "Hello, " + name;
.. };
|> print greet("Mac");
Hello, Mac

Keyboard Shortcuts

KeyAction
Up / DownNavigate history
Left / RightMove cursor
Option+Left / RightMove by word
Ctrl+AMove to start of line
Ctrl+EMove to end of line
Ctrl+WDelete previous word
Option+DeleteDelete next word
Ctrl+KDelete to end of line
Ctrl+UDelete entire line
Ctrl+LClear screen
Ctrl+CCancel current input
Ctrl+DExit REPL

History

Command history is saved to ~/.mac_history and persists across sessions. Each line of multi-line input is stored individually for easier recall.

Auto-Semicolons

Simple expressions without a trailing ; or } get a semicolon appended automatically.

|> print "no semicolon needed"
no semicolon needed

Pasting

Multi-line code can be pasted directly. Newlines in pasted text are processed through the same brace-tracking as typed input.

Commands

CommandAction
exitExit the REPL
clearClear the screen

Piped Input

When stdin is not a terminal, Mac reads all input as a batch and executes it.

echo 'print "hello";' | mac
# Output: hello

Command-Line Flags

Usage

mac [script]
mac --analyze <file>
mac --uninstall
mac --version

Modes

Run Script

mac script.mac

Execute a .mac file. Bare filenames save to ~/mac/output/ by default, while explicit relative and absolute paths are respected. Temporary effect files are cleaned up after execution.

Environment

MAC_OUTPUT_DIR

MAC_OUTPUT_DIR=/tmp/mac-out mac script.mac

Redirect all saves into a single directory. When this is set, Mac strips any path components from the requested save path and writes the file into MAC_OUTPUT_DIR using only its filename.

This is primarily intended for hosts like web apps and editors that need to control where generated files land. The saved confirmation still prints the actual absolute path so users can find the output immediately.

Interactive REPL

mac

Start the interactive prompt. See REPL.

Analyze (LSP)

mac --analyze file.mac

Output a JSON analysis of the file to stdout. Used by the VS Code extension for IDE features (diagnostics, completions, hover info, semantic highlighting).

The JSON contains 11 categories: symbols, references, diagnostics, properties, foldingRanges, semanticTokens, paramHints, chainHints, signatures, classes, templates.

Version

mac --version
mac -v

Print the version string (e.g., Mac v0.2.3).

Uninstall

mac --uninstall

Remove the Mac installation. See Installation.

See Also