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
| Feature | Description |
|---|---|
| Dynamic typing | Values are strings, numbers, booleans, nil, arrays, or maps |
| First-class functions | Functions are values, with closures, lambdas, and implicit returns |
| Classes | Single inheritance, constructors, methods, fields |
| Enums | Exhaustive sum types with match destructuring (Result, Option) |
| Pattern matching | match expr { Pattern -> result } with enum destructuring |
| String interpolation | "Hello, {name}!" embeds expressions in strings |
| Destructuring | var [a, b] = expr and for (var [k, v] in pairs) |
| Immutable bindings | val x = 42 prevents reassignment |
| Pipe operator | value |> func for pipeline-style composition |
| Partial application | partial(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 |
| Effects | blur(5), sepia, grayscale — composable image transforms |
| Grid layout | grid 2x2 { ... } for multi-panel compositions |
| GIF animation | gif { ... } with per-frame timing, transitions, and easing curves |
| Save operator | expr => "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
| Type | Example | Description |
|---|---|---|
number | 42, 3.14 | IEEE 754 double-precision floating point |
string | "hello" | Double-quoted UTF-8 string |
bool | true, false | Boolean |
nil | nil | Absence 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
| Type | Example | Description |
|---|---|---|
[T] | [1, 2, 3] | Ordered, mutable array |
{T} | {"a": 1} | Key-value map (string keys) |
See Arrays and Maps for details.
Enum Types
| Type | Example | Description |
|---|---|---|
enum | enum 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
| Type | Produced by | Description |
|---|---|---|
Meme | @template "text" | Renderable meme image |
Gif | gif { ... } | Animated GIF (supports transitions) |
RenderSurface | Effects pipeline | In-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
| Operator | Description | Example |
|---|---|---|
+ | Addition / string concatenation | 2 + 3 → 5, "a" + "b" → "ab" |
- | Subtraction / negation | 5 - 3 → 2, -x |
* | Multiplication | 4 * 3 → 12 |
/ | Division | 10 / 4 → 2.5 |
% | Modulo | 7 % 3 → 1 |
Comparison
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
Comparisons return true or false.
Logical
| Operator | Description |
|---|---|
and | Logical AND (short-circuit) |
or | Logical 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:
| Precedence | Operators |
|---|---|
| 1 | => (save) |
| 2 | |> (pipe) |
| 3 | >> (compose) |
| 4 | or |
| 5 | and |
| 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 -- anonymous functions
- Pipe operator --
value |> func - Compose operator --
func1 >> func2
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
- Functions -- named function declarations
- Native Functions --
map,filter,reduce
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
- Prelude Classes -- built-in Size, Duration, Meme, Gif
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
- Native Functions -- full list of array operations
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 -- text color, outline, font
- Effects -- image transforms
- Templates -- built-in template list
- Saving Output --
=>andsave()
Styles
Style blocks define reusable text rendering configurations.
Syntax
style name {
property: value
property: value
}
Properties
| Property | Type | Default | Description |
|---|---|---|---|
color | hex string | "#FFFFFF" | Text color (#RRGGBB or #RRGGBBAA) |
outline | number | 3 | Outline stroke width in pixels |
outlineWidth | number | 3 | Alias for outline |
outlineColor | hex string | "#000000" | Outline color |
shadow | number | 0 | Drop shadow offset in pixels |
shadowColor | hex string | "#000000" | Shadow color |
fontSize | number or string | "md" | Font size: pixels or "sm", "md", "lg", "xlg" |
fontWeight | string | "bold" | "normal" or "bold" |
textTransform | string | "uppercase" | "none" or "uppercase" |
background | hex string | none | Background 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
| Preset | Approximate 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:
| Keyword | Effect |
|---|---|
impact | White text, black outline, bold, uppercase |
cinematic | Letter-boxed look |
shout | Large bold uppercase |
whisper | Small, normal weight |
panic | Red text, heavy outline |
See Also
- Meme Literals -- using styles with memes
- Effects -- image-level transforms
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.
| Effect | Description |
|---|---|
grayscale | Convert to grayscale |
sepia | Warm sepia tone |
invert | Invert all colors |
sharpen | Sharpen edges |
vignette | Darken edges |
@blank "Test" |> grayscale => "gray.png";
Parameterized Effects
These take a numeric argument and return a function that transforms a meme.
| Effect | Parameter | Description |
|---|---|---|
blur(radius) | 1-20 | Gaussian blur |
pixelate(blockSize) | 2-50 | Pixelation |
noise(amount) | 0.0-1.0 | Random noise overlay |
saturate(factor) | 0.0-5.0 | Color saturation (1.0 = normal) |
contrast(factor) | 0.0-5.0 | Contrast (1.0 = normal) |
brightness(factor) | 0.0-3.0 | Brightness (1.0 = normal) |
hueShift(degrees) | 0-360 | Rotate hue |
glow(radius) | 1-20 | Bloom/glow effect |
posterize(levels) | 2-32 | Reduce color levels |
chromatic(offset) | 1-20 | RGB channel displacement |
threshold(level) | 0-255 | Black/white binarization |
tint(hexColor) | hex string | Color tint overlay |
jpeg(quality) | 1-100 | JPEG 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
| Unit | Example | Description |
|---|---|---|
ms | 500ms | Milliseconds |
s | 1.5s | Seconds (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
| Transition | Description |
|---|---|
crossfade | Blend between frames |
slideLeft | New frame slides in from right |
slideRight | New frame slides in from left |
slideUp | New frame slides in from bottom |
slideDown | New frame slides in from top |
wipe | Horizontal wipe reveal |
fadeBlack | Fade through black |
zoom | Zoom 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";
| Easing | Description |
|---|---|
linear | Constant speed (default) |
easeIn | Accelerate from rest |
easeOut | Decelerate to rest |
easeInOut | Accelerate then decelerate |
bounce | Bouncing 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
- Grid Layout -- grid frames in GIFs
- Effects
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:
| Input | Output 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 path | Actual 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
| Extension | Format |
|---|---|
.png | PNG (lossless, default) |
.jpg / .jpeg | JPEG (lossy) |
.gif | GIF (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
| Function | Signature | Returns | Description |
|---|---|---|---|
clock | clock() | number | Wall-clock time in seconds |
type | type(value) | string | Runtime type name |
String Operations
| Function | Signature | Returns | Description |
|---|---|---|---|
len | len(str) | number | String length (also works on arrays) |
substr | substr(str, start, length) | string | Extract substring |
split | split(str, delimiter) | [string] | Split into array |
join | join(array, separator) | string | Join array into string |
upper | upper(str) | string | Convert to uppercase |
lower | lower(str) | string | Convert to lowercase |
trim | trim(str) | string | Remove surrounding whitespace |
replace | replace(str, from, to) | string | Replace all occurrences |
Math
| Function | Signature | Returns | Description |
|---|---|---|---|
sqrt | sqrt(n) | number | Square root |
abs | abs(n) | number | Absolute value |
pow | pow(base, exp) | number | Exponentiation |
floor | floor(n) | number | Round down |
ceil | ceil(n) | number | Round up |
Array Mutation
| Function | Signature | Returns | Description |
|---|---|---|---|
push | push(array, value) | nil | Append element (mutates) |
pop | pop(array) | T | Remove and return last element |
Array Transforms
| Function | Signature | Returns | Description |
|---|---|---|---|
map | map(array, fn) | [T] | Transform each element |
filter | filter(array, fn) | [T] | Keep matching elements |
reduce | reduce(array, fn, init) | T | Fold with accumulator |
find | find(array, fn) | T | First matching element |
flatMap | flatMap(array, fn) | [T] | Map and flatten one level |
flatten | flatten(array) | [T] | Flatten one nesting level |
reverse | reverse(array) | [T] | Reversed copy |
sort | sort(array) | [T] | Sorted copy |
sort | sort(array, cmp) | [T] | Sort with comparator |
take | take(array, n) | [T] | First n elements |
drop | drop(array, n) | [T] | Skip first n elements |
zip | zip(a, b) | [[A, B]] | Pair elements from two arrays |
enumerate | enumerate(array) | [[number, T]] | Index-value pairs |
takeWhile | takeWhile(array, fn) | [T] | Take elements while predicate is true |
dropWhile | dropWhile(array, fn) | [T] | Drop elements while predicate is true |
partition | partition(array, fn) | [[T], [T]] | Split by predicate |
groupBy | groupBy(array, fn) | {string: [T]} | Group by key function |
unique | unique(array) | [T] | Remove duplicates |
chunk | chunk(array, n) | [[T]] | Split into groups of n |
scan | scan(array, fn, init) | [T] | Reduce keeping intermediates |
each | each(array, fn) | nil | Side-effect iteration |
Array Predicates
| Function | Signature | Returns | Description |
|---|---|---|---|
any | any(array, fn) | bool | Any element matches |
all | all(array, fn) | bool | All elements match |
Generators
| Function | Signature | Returns | Description |
|---|---|---|---|
range | range(end) | [number] | [0, 1, ..., end-1] |
range | range(start, end) | [number] | [start, ..., end-1] |
range | range(start, end, step) | [number] | With custom step |
Functional Utilities
| Function | Signature | Returns | Description |
|---|---|---|---|
partial | partial(fn, ...args) | function | Bind arguments to a function, returning a new function with fewer parameters |
User Input
| Function | Signature | Returns | Description |
|---|---|---|---|
input | input(prompt) | string | Read 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
| Function | Signature | Returns | Description |
|---|---|---|---|
beside | beside(left, right) | Meme | Side-by-side |
stack | stack(top, bottom) | Meme | Vertical stack |
grid | grid(cols, memes) | Meme | Grid arrangement |
pad | pad(meme, px) | Meme | Add white padding |
border | border(meme, px) | Meme | Add black border |
Animation
| Function | Signature | Returns | Description |
|---|---|---|---|
animate | animate(memes, ms) | Gif | Create GIF with uniform frame duration |
toGrid | toGrid(memes, cols, rows) | Meme | Render array to grid |
I/O
| Function | Signature | Returns | Description |
|---|---|---|---|
save | save(target, path) | bool | Save 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
| Method | Returns | Description |
|---|---|---|
init(w, h) | Size | Constructor |
__add__(other) | Size | Add two sizes |
__mul__(n) | Size | Multiply by scalar |
__eq__(other) | bool | Equality 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)
| Method | Returns | Description |
|---|---|---|
init(ms) | Duration | Constructor |
__add__(other) | Duration | Add durations |
__mul__(n) | Duration | Multiply by scalar |
__eq__(other) | bool | Equality 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");
| Method | Returns | Description |
|---|---|---|
init(template) | Meme | Constructor with Template |
text(position, str) | Meme | Add text at position (chainable) |
resize(size) | Meme | Create resized copy |
save(format, path) | nil | Save 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");
| Method | Returns | Description |
|---|---|---|
init() | Gif | Constructor |
frame(meme, duration) | Gif | Add frame (chainable) |
transition(type, duration) | Gif | Add transition between frames (chainable) |
save(path) | nil | Save as animated GIF |
__add__(frame) | Gif | Add 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
- Meme Literals --
@templateshorthand - GIF Animation --
gif { }block syntax
Templates
Built-in meme templates available by name with the @ prefix.
Built-in Templates
| Template | Description | Text Slots |
|---|---|---|
blank | White canvas | top, bottom, center |
dark | Dark/black canvas | top, bottom, center |
two_panel | Two-panel vertical split | top, bottom |
three_panel | Three-panel vertical | top, center, bottom |
four_panel | 2x2 grid | (4 entries) |
bottom_text | Image with bottom caption | bottom |
caption_bar | White bar caption above image | top, bottom |
wide | Wide aspect ratio canvas | top, bottom, center |
tall | Tall aspect ratio canvas | top, bottom, center |
square | Square canvas | top, 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
- Meme Literals -- using templates
- Styles -- text customization
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
| Platform | Architecture |
|---|---|
| macOS | arm64 (Apple Silicon) |
| macOS | x86_64 (Intel) |
| Linux | x86_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
| Path | Contents |
|---|---|
~/.mac/ | Binary, assets, stdlib |
~/.local/bin/mac | Symlink to binary |
~/.mac_history | REPL command history |
~/.mac/update_check | Update 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
| Prompt | Meaning |
|---|---|
|> | 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
| Key | Action |
|---|---|
| Up / Down | Navigate history |
| Left / Right | Move cursor |
| Option+Left / Right | Move by word |
| Ctrl+A | Move to start of line |
| Ctrl+E | Move to end of line |
| Ctrl+W | Delete previous word |
| Option+Delete | Delete next word |
| Ctrl+K | Delete to end of line |
| Ctrl+U | Delete entire line |
| Ctrl+L | Clear screen |
| Ctrl+C | Cancel current input |
| Ctrl+D | Exit 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
| Command | Action |
|---|---|
exit | Exit the REPL |
clear | Clear 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.