Mog: A Programming Language for AI Agents
What if an AI agent could modify itself quickly, easily, and safely? Mog is a programming language designed for exactly this.
Overview
Mog is a statically typed, compiled, embedded language (think statically typed Lua) designed to be written by LLMs – the full spec fits in 3200 tokens.
- An AI agent writes a Mog program, compiles it, and dynamically loads it as a plugin, script, or hook.
- The host controls exactly which functions a Mog program can call (capability-based permissions), so permissions propagate from agent to agent-written code.
- Compiled to native code for low-latency plugin execution – no interpreter overhead, no JIT, no process startup cost.
- The compiler is being rewritten in safe Rust so the entire toolchain can be audited for security.
- Even without a full security audit, Mog is already useful for agents extending themselves with their own code.
- MIT licensed, contributions welcome. https://github.com/voltropy/mog
Why Mog?
A general-purpose AI agent should be able to continuously extend and modify itself. Over time, an agent should grow into a personal server that manages tasks in all kinds of ways. To do that, the agent needs to write its own code – and that code needs to be safe.
The simplest kind of program an agent writes is a one-off script to achieve some task. Examples:
- Converting a markdown file to PDF.
- Analyzing a CSV with database results.
- Sending test requests to an application’s HTTP endpoint.
- Renaming all the files in a folder.
- Installing dependencies.
Coding agents typically use bash for this, and sometimes reach for an inline Python or TypeScript script. Mog is well-suited for this: it’s easy to write and it compiles fast. Notably, scripting is one of the main ways an agent escapes its sandbox, and Mog closes that loophole. Even if Mog is given a capability by its host agent to call bash commands, the host still has the ability to filter those commands according to its permissions, just as if the model had called its bash tool directly.
The second kind of program agents commonly write is a hook: a piece of code that runs repeatedly at a certain point in the agentic loop. Pre- and post-tool-use hooks are common, as well as pre-compaction hooks. For a hook, it’s not important for it to compile quickly, but it needs to start up quickly and execute quickly, since it can get called frequently enough that a slow implementation would meaningfully drag down the user experience. Mog compiles to native code, and it can then load that machine code into the agent’s running binary. The key property that makes this safe: native code compiled by the Mog compiler can’t do anything other than what the host explicitly lets it do – not even exceed limits on memory or time. The agent can incorporate a Mog program into itself at runtime and call into it without inter-process communication overhead or process startup latency.
The third category is writing or rewriting parts of the agent itself. This could mean adding a new tool that the agent exposes to the LLM, a status line for the user interface, settings about agent skills or session management – a potentially long list of agent internals that would benefit from extension or specialization.
One could imagine a microkernel agent, written as a small backbone of Rust code that calls into Mog code for almost all its features. The kernel would manage the event loops, multi-threading, root permissions, compiling and loading Mog programs, and maybe part of its upgrade functionality, but the rest of the system could be written in Mog – running with minimal, granular permissions and upgradeable on the fly without a restart, just by prompting the agent to modify itself.
Alternatives
Without something like Mog, every option for AI-generated agent code has a downside. One major one is enforcing permissions: tools like Jeffrey Emanuel’s dcg can interdict rm -rf and similarly destructive shell commands, but they can’t stop an agent from emitting Python that iterates through files in a folder and calls os.remove() on each one.
The next step is generally to run the agent in a sandbox, like a Docker container. But then the permissions tend to apply to the whole sandbox, so letting the agent use the host computer in nontrivial ways (e.g. pull in environment variables, access CLI tools, drive a browser, make HTTP requests) requires opening up the sandbox boundary. At that point the agent has regained essentially unfettered access to that capability.
What’s missing is a way to propagate the permissions granted to an agent to the programs that agent writes. Mog addresses this directly.
Another issue with external scripts is sharing them – receiving a script from an untrusted source is a security risk. With Mog, the receiving agent compiles the Mog source itself, rather than running a pre-compiled binary that could take over the machine.
There are other approaches to LLM-driven extension systems. Jeffrey Emanuel is building something similar using JavaScript containers with permissions, which is quite close to Mog in spirit, and has an impressive level of infrastructure behind it. Mog could complement such systems, especially for higher-performance plugins – Mog’s infrastructure is also in Rust, making integration straightforward. Emanuel’s Rust version of the Pi agent would be a natural starting point for a Mog-based microkernel agent.
WASM is another natural option, since it’s a more standard sandboxing technique. Mog takes a different approach: a hot loop over an array runs at native speed, without the unpredictability of JIT compilation, the overhead of WASM interpretation, or the process startup time of an external binary.
The Language
Mog is a minimal language designed to be written by LLMs. Its full specification, with examples, fits in a single 3200-token markdown file, which an LLM can pin in context when writing Mog programs. Everything about the language is designed to optimize the latency between asking an LLM to write a program for a task and having a performant program that implements that task.
The primary goal of Mog syntax is to be familiar to LLMs. It’s a mix of TypeScript, Rust, and Go, with a couple of Pythonisms. Its module system is modeled on Go modules. It’s a small but usable language with no foot-guns: no implicit type coercion, no operator precedence, and no uninitialized data.
Why statically typed and compiled? Because latency matters in this domain. Consider an agent plugin that runs before every tool call – it needs to be fast, or it’s not worth using. Jeffrey Emanuel rewrote every single one of his agent plugins in Rust to reduce startup time and Python overhead. A modern computer is fast, but Python is not. Even Bun’s startup time, which has been heavily optimized, is nowhere near the startup time of a Rust program, and even that is slow compared to calling into an in-process library.
Example
Here’s a Mog hook that an agent might write to monitor its own tool calls for errors:
requires fs; // must be provided by host
optional log; // runs without it
pub fn on_tool_result(tool_name: string, stderr: string) {
if (stderr.contains("permission denied")) {
log.warn(f"{tool_name}: permission denied");
fs.append_file("agent.log", f"[warn] {tool_name}: {stderr}\n");
}
}The first two lines tell the security story: this hook can append to a file and optionally log, but it cannot make HTTP requests, run shell commands, or read environment variables. The host decides what this program is allowed to do – it can even interdict the fs.append_file() call if this program doesn’t have that permission.
To learn more about how to use Mog, see the detailed Mog Language Guide.
As agents get more complex and more of their code is bespoke, performance, correctness, and security of the combined system become harder to maintain. Mog is designed to address these issues.
The Capability System
Mog is an embedded language. It runs inside a host program, much like Lua – the way a Mog host provides functions to the guest Mog program is based directly on Lua’s elegant and battle-tested design. By itself, a Mog program cannot do any I/O, perform syscalls, or access raw memory. It can only run functions and return values. That is the extent of Mog’s sandbox.
Any I/O, FFI, syscalls, or interaction with other systems can only be done by calling a host function – a function that the host makes available to the Mog program. This is the essence of Mog’s capability system: the host decides exactly which functions it will allow the guest to call. The host is also free to filter the inputs to such a function and to filter the response delivered back to the Mog program.
Part of this isolation involves preventing a Mog program from taking over the host process in subtler ways. An upcoming feature will allow the host to control whether a Mog program can request a larger memory arena, preventing the guest from consuming all available RAM. An existing feature is cooperative interrupt polling: Mog loops all have interrupt checks added at back-edges, which allows the host to halt the guest program without killing the process. This enables timeout enforcement. There is no way for a guest program to corrupt memory or kill the process (assuming correct implementation of the compiler and host).
Since a typical agent runs an event loop, Mog programs are designed to run inside an event loop, familiar to anyone who has written JavaScript or TypeScript. Mog’s support for this consists primarily of async/await syntax. Mog programs can define async functions, and importantly, the host can also provide async functions that the guest can call. This allows a guest program to fire off an HTTP request and a timer and do something different depending on which one finishes first – internally the compiler implements this using coroutine lowering, based on LLVM’s design for the same.
The Compiler
The compiler uses QBE as its code generation backend, which is being rewritten in safe Rust. Once complete, the entire toolchain for compiling, loading, and running a Mog program will be in Rust – the goal is for all of it to be safe Rust, making it much more difficult to find an exploit in the toolchain.
The first implementation of Mog used LLVM as the backend. LLVM can produce somewhat faster code due to its wide array of optimizations, but it had two major issues. First, compile times were not fast enough. The new compiler has compile times that are not quite as good as Go’s, but within an order of magnitude for programs under 1000 lines – fast enough that the start time for one-off scripts is not painful. Mog does not claim to provide zero-cost abstractions or arbitrary opportunities for low-level optimization. It compiles to native code, but an expert can still write faster C or C++.
The second issue with LLVM is that for Mog, the compiler itself is part of the trusted computing base. If the compiler has a security flaw, the agent has a security flaw. This rules out an enormous codebase like LLVM’s. The compiler needs to be small enough to control and audit.
Current Status
Mog was created entirely using the Volt coding agent, the vast majority of which used a single continuous session spanning over three weeks, using Voltropy’s Lossless Context Management to maintain its memory after compactions. This session is still running, porting the QBE compiler to safe Rust. The models used were Claude Opus 4.6, Kimi k2.5, and GLM-4.7.
Significant work remains to standardize the functions that hosts provide to Mog programs. This should include much of what the Python standard library provides: support for JSON, CSV, SQLite, POSIX filesystem and networking operations, etc. The Mog standard library should also provide async functions for calling LLMs – both high-level interfaces (like Simon Willison’s llm CLI tool or DSPy modules) and low-level interfaces that allow fine-grained context management.
To be clear: Mog has not been audited, and it is presented without security guarantees. It should be possible to secure it, but that work has not yet been done. There are tests that check that a malicious Mog program cannot access host functionality that the host does not want to provide, but this design has enough security attack surface to warrant careful scrutiny.
Even without audited security properties, Mog is already useful for extending AI agents with their own code – they’re not trying to exploit themselves. For untrusted third-party code, treat the security model as unverified until a formal audit is completed.
The Mog Language Guide
A complete guide to learning and using the Mog programming language. Jump to Code Examples
Chapter 1: Your First Mog Program
This chapter walks through the basics of writing and running Mog code. By the end, you will understand how a Mog program is structured, how to print output, how to write comments, and how to define and call functions.
Hello, World
Every Mog program needs a main function. Here is the simplest complete program:
fn main() -> int {
println("Hello, world!");
return 0;
}fn main() -> int declares the entry point. The -> int annotation means the function returns an integer — by convention, 0 signals success. The println function prints a string followed by a newline. The return 0; statement exits the program.
Semicolons are required after every statement. Curly braces delimit blocks. If you are coming from Go or Rust, this will feel natural. If you are coming from Python, the semicolons may take a few minutes to get used to.
How Mog Programs Are Compiled and Run
Mog is a compiled language. You write a .mog source file, compile it to a native executable, and run the executable. The compiler is written in Rust:
# Build the compiler (once)
cargo build --release --manifest-path compiler/Cargo.toml
# Compile a Mog program to a native executable
mogc hello.mog
# Run the resulting binary
./helloThe compilation pipeline works like this: the Rust compiler reads your .mog file, lexes it into tokens, parses it into an abstract syntax tree, analyzes and type-checks it, generates QBE intermediate language, and passes it to rqbe (a safe Rust QBE backend that runs in-process). rqbe emits assembly, which the system assembler and linker turn into a native binary linked with the Mog runtime. The whole process takes milliseconds for small programs.
There is also a convenience script that compiles, links, and runs in a single step:
./algb hello.mogIn production, Mog programs run embedded inside a host application. The host compiles the script, registers capabilities, and invokes the compiled code through a C API. But for learning the language, the standalone compilation path is all you need.
There is a third compilation mode: plugins. You can compile a .mog file into a shared library (.dylib on macOS, .so on Linux) instead of a standalone executable. The host loads the library at runtime with dlopen, queries what functions are available, and calls them by name. Functions marked pub in the source become exported symbols; everything else gets internal linkage and is invisible to the loader. This is the right path when you want pre-compiled, hot-swappable modules — the host never sees the source code, just a binary it can load and unload. See Chapter 14 for the full plugin API.
The compiler uses rqbe, a safe Rust implementation of the QBE backend, as its code generation engine. rqbe runs entirely in-process — no external tools are needed beyond the system assembler and linker. It compiles fast and produces correct native code for ARM64 and x86.
Program Structure
A Mog source file is a sequence of top-level declarations followed by a main function. Top-level declarations include functions, structs, and capability requirements. You cannot put executable statements at the top level — all code runs inside functions.
// Top-level: struct declaration
struct Point {
x: float,
y: float
}
// Top-level: function declaration
fn distance(a: Point, b: Point) -> float {
dx := a.x - b.x;
dy := a.y - b.y;
return sqrt((dx * dx) + (dy * dy));
}
// Top-level: entry point
fn main() -> int {
p1 := Point { x: 3.0, y: 0.0 };
p2 := Point { x: 0.0, y: 4.0 };
d := distance(p1, p2);
println(f"Distance: {d}");
return 0;
}The order of declarations does not matter — you can call a function that is defined later in the file. The compiler resolves all names before generating code.
Here is a program with multiple functions that call each other:
fn square(x: int) -> int {
return x * x;
}
fn sum_of_squares(a: int, b: int) -> int {
return square(a) + square(b);
}
fn main() -> int {
result := sum_of_squares(3, 4);
println(f"3² + 4² = {result}"); // 3² + 4² = 25
return 0;
}And a program that uses a struct and a helper function:
struct Rectangle {
width: float,
height: float
}
fn area(r: Rectangle) -> float {
return r.width * r.height;
}
fn main() -> int {
r := Rectangle { width: 5.0, height: 3.0 };
a := area(r);
print_string("Area: ");
print_f64(a);
println("");
return 0;
}Comments
Mog supports two kinds of comments. Single-line comments start with // and run to the end of the line. Multi-line comments are delimited by /* and */.
// This is a single-line comment
/* This is a
multi-line comment */
fn main() -> int {
// Comments can appear on their own line
x := 42; // or at the end of a line
/*
* Multi-line comments are useful for temporarily
* disabling blocks of code or writing longer
* explanations.
*/
println(f"x = {x}");
return 0;
}Multi-line comments do not nest. A /* inside a multi-line comment is treated as ordinary text, and the first */ ends the comment. In practice, single-line comments are far more common.
fn main() -> int {
// Calculate the sum of integers from 1 to 10
sum := 0;
i := 1;
while (i <= 10) {
sum = sum + i;
i = i + 1;
}
println(f"Sum: {sum}"); // Sum: 55
/* Temporarily disabled:
println("This line does not execute");
*/
return 0;
}Print Functions
Mog provides several built-in functions for printing output. These are always available — no imports needed.
| Function | Description |
|---|---|
println(s) |
Print a string followed by a newline |
print_string(s) |
Print a string without a newline |
print(n) |
Print an integer |
println_i64(n) |
Print an integer followed by a newline |
print_f64(x) |
Print a float |
The most common is println, which prints a string with a trailing newline. For formatted output, combine it with f-string interpolation:
fn main() -> int {
println("Hello, world!");
name := "Mog";
println(f"Welcome to {name}!");
x := 42;
println(f"The answer is {x}");
return 0;
}Output:
Hello, world!
Welcome to Mog!
The answer is 42When you need to print numbers without f-string formatting, use the type-specific print functions:
fn main() -> int {
// Print an integer
print_string("Count: ");
println_i64(42);
// Print a float
print_string("Pi: ");
print_f64(3.14159);
println("");
// print() also works for integers
print_string("Score: ");
print(100);
println("");
return 0;
}Output:
Count: 42
Pi: 3.141590
Score: 100The print_string function is useful when you want to build a line of output from multiple parts without newlines between them:
fn main() -> int {
print_string("Loading");
print_string(".");
print_string(".");
print_string(".");
println(" done!");
return 0;
}Output:
Loading... done!F-string interpolation (the f"..." syntax) is the preferred way to format output in Mog. It can embed any expression inside {} delimiters:
fn main() -> int {
width := 10;
height := 5;
println(f"Rectangle: {width} x {height} = {width * height}");
name := "Alice";
score := 95;
println(f"{name} scored {score} points");
a := 3.14;
println(f"Value: {a}");
return 0;
}Output:
Rectangle: 10 x 5 = 50
Alice scored 95 points
Value: 3.14A More Complete Example
Let’s put everything together. Here is a program that defines a few functions, uses variables and arithmetic, and prints formatted output:
fn factorial(n: i64) -> i64 {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
fn fibonacci(n: i64) -> i64 {
if (n <= 0) { return 0; }
if (n == 1) { return 1; }
a: i64 = 0;
b: i64 = 1;
i: i64 = 2;
while (i <= n) {
temp: i64 = a + b;
a = b;
b = temp;
i = i + 1;
}
return b;
}
fn main() -> int {
// Factorial
println(f"5! = {factorial(5)}");
println(f"10! = {factorial(10)}");
// Fibonacci
println(f"fib(10) = {fibonacci(10)}");
println(f"fib(20) = {fibonacci(20)}");
// Simple arithmetic
x := 42;
y := 13;
println(f"{x} + {y} = {x + y}");
println(f"{x} - {y} = {x - y}");
println(f"{x} * {y} = {x * y}");
println(f"{x} / {y} = {x / y}");
println(f"{x} % {y} = {x % y}");
return 0;
}Output:
5! = 120
10! = 3628800
fib(10) = 55
fib(20) = 6765
42 + 13 = 55
42 - 13 = 29
42 * 13 = 546
42 / 13 = 3
42 % 13 = 3A few things to notice:
- Functions are defined with
fn, parameters have type annotations, and the return type follows->. - The
factorialfunction calls itself recursively. Mog supports recursion naturally. - The
fibonaccifunction uses awhileloop with mutable variables. The:=operator is used inside the loop (i = i + 1) to reassign. Variables declared with an explicit type annotation likea: i64 = 0can be reassigned with=. - F-strings can embed arbitrary expressions:
{x + y}computes the sum inline. - Integer division (
/) truncates toward zero, and%gives the remainder.
Here is one more example — a program that computes the sum of the first N squares:
fn sum_of_squares(n: int) -> int {
total := 0;
for i in 1..n + 1 {
total = total + (i * i);
}
return total;
}
fn main() -> int {
for n in [5, 10, 20, 100] {
println(f"Sum of squares(1..{n}) = {sum_of_squares(n)}");
}
return 0;
}Output:
Sum of squares(1..5) = 55
Sum of squares(1..10) = 385
Sum of squares(1..20) = 2870
Sum of squares(1..100) = 338350This example shows for-in loops over both ranges (1..n + 1) and arrays ([5, 10, 20, 100]). We will cover control flow in detail in a later chapter — for now, the syntax should be readable.
What Mog Is Not
Mog is deliberately not many things. Each omission keeps the language small, the security model tractable, and the compilation fast:
- Not a systems language. No raw pointers, no manual memory management, no POSIX syscalls, no direct OS access.
- Not standalone. Mog is always embedded in a host application. There is no standard library for file I/O or networking — the host provides everything.
- Not general-purpose. Mog is for scripts, plugins, and orchestration. It is not designed for building web servers, operating systems, or databases.
- Not object-oriented. No classes, no inheritance, no methods on types. Structs hold data; functions operate on data. Higher-order functions and closures provide the abstraction mechanisms.
- No macros or metaprogramming. The language you see is the language that runs. No code generation, no compile-time evaluation, no syntax extensions.
- No generics. Beyond tensor dtype parameterization (
tensor<f32>,tensor<f16>), there are no generic types or functions. This keeps the type system simple and the compiler small. - No exceptions with stack unwinding. Error handling uses
Result<T>with explicit propagation via?. Errors are values, not control flow. - No threads or locks. Concurrency is cooperative via
async/await, with the host managing the event loop.
If you need any of these features, Mog is probably not the right language for your use case — and that’s fine. Mog is designed to do a few things well rather than everything adequately.
What’s Next
You now know how to write, compile, and run a Mog program. You have seen the basic program structure, comments, print functions, and how to define and call functions with typed parameters. Chapter 2 covers variables and bindings in depth — how := and = work, type annotations, and scoping rules.
Chapter 2: Variables and Bindings
Mog keeps variable declaration simple: there are no var, let, or const keywords. You create bindings with := and reassign them with =. That’s it.
Creating Bindings with :=
The := operator declares a new variable and assigns it a value. The type is inferred from the right-hand side:
name := "Alice";
age := 30;
score := 95.5;
active := true;Every binding needs an initial value — there are no uninitialized variables in Mog.
fn main() {
greeting := "hello, world";
count := 0;
pi := 3.14159;
print(greeting);
print(count);
print(pi);
}You can create bindings in sequence, and later bindings can reference earlier ones:
fn main() {
width := 10;
height := 20;
area := width * height;
print("area = {area}"); // area = 200
}Bindings work inside any block scope — function bodies, if branches, loop bodies:
fn main() {
x := 42;
if x > 0 {
label := "positive";
print(label);
}
// label is not accessible here
}Reassignment with =
Once a variable exists, use = to change its value. All variables in Mog are mutable:
fn main() {
count := 0;
count = 1;
count = count + 1;
print(count); // 2
}The distinction matters: := creates, = updates. Using = on a name that doesn’t exist is a compile error. Using := on a name that already exists in the same scope creates a new shadowed binding (see below).
fn main() {
x := 10;
x = 20; // reassignment — fine
x = x * 2; // reassignment — x is now 40
print(x); // 40
}A common pattern is accumulating a result in a loop:
fn main() {
total := 0;
for i in 1..11 {
total = total + i;
}
print(total); // 55
}Building a string incrementally:
fn main() {
result := "";
names := ["Alice", "Bob", "Charlie"];
for i, name in names {
if i > 0 {
result = result + ", ";
}
result = result + name;
}
print(result); // Alice, Bob, Charlie
}Type Annotations
Mog infers types from the right-hand side, but you can annotate explicitly when you want to be clear about intent:
x := 42; // inferred as int
x: int = 42; // explicit — same result
ratio := 3.14; // inferred as float
ratio: float = 3.14; // explicit — same result
name := "Mog"; // inferred as string
name: string = "Mog"; // explicitExplicit annotations are useful when the default inference isn’t what you want:
// Without annotation, 42 is int
n := 42;
// With annotation, you can specify a different integer type
n: i32 = 42;
n: u64 = 42;They also help document intent in longer functions:
fn process_data(items: [string]) -> int {
count: int = 0;
total_length: int = 0;
for item in items {
count = count + 1;
total_length = total_length + item.len;
}
average: float = total_length as float / count as float;
print("average length: {average}");
return count;
}Note: function parameters and return types always require type annotations. There is no inference for function signatures:
fn add(a: int, b: int) -> int {
return a + b;
}Shadowing
Using := with a name that already exists in the current scope creates a new binding that shadows the old one. The old value is no longer accessible:
fn main() {
x := 10;
print(x); // 10
x := "hello"; // shadows the previous x — this is a new binding
print(x); // hello
}Shadowing is useful when you want to transform a value and keep the same name:
fn main() {
input := " hello world ";
input := input.trim();
input := input.upper();
print(input); // HELLO WORLD
}Each := creates a genuinely new variable. The shadowed and shadowing variables can have different types:
fn main() {
value := 42; // int
value := str(value); // string — shadows the int
print(value); // "42"
}Inner scopes can also shadow outer variables without affecting them:
fn main() {
x := "outer";
if true {
x := "inner"; // shadows outer x inside this block
print(x); // inner
}
print(x); // outer — unchanged
}Compare this to reassignment, which modifies the existing variable:
fn main() {
x := "outer";
if true {
x = "modified"; // reassigns the outer x
}
print(x); // modified
}Practical Examples
Swapping Two Values
fn main() {
a := 10;
b := 20;
temp := a;
a = b;
b = temp;
print("a = {a}, b = {b}"); // a = 20, b = 10
}Fibonacci Sequence
fn fibonacci(n: int) -> int {
if n <= 1 { return n; }
a := 0;
b := 1;
for i in 2..n+1 {
temp := a + b;
a = b;
b = temp;
}
return b;
}
fn main() {
for i in 0..10 {
print(fibonacci(i));
}
}Processing a List
fn main() {
scores := [85, 92, 78, 95, 88];
sum := 0;
max_score := scores[0];
min_score := scores[0];
for score in scores {
sum = sum + score;
if score > max_score {
max_score = score;
}
if score < min_score {
min_score = score;
}
}
average := sum as float / scores.len as float;
print("average: {average}");
print("max: {max_score}");
print("min: {min_score}");
}Counting Occurrences
fn count_char(s: string, target: string) -> int {
count := 0;
parts := s.split("");
for ch in parts {
if ch == target {
count = count + 1;
}
}
return count;
}
fn main() {
text := "hello world";
l_count := count_char(text, "l");
print("l appears {l_count} times"); // l appears 3 times
}Summary
| Syntax | Meaning |
|---|---|
x := 42; |
Create a new binding (type inferred) |
x: int = 42; |
Create a new binding (type explicit) |
x = 100; |
Reassign an existing binding |
x := "hi"; |
Shadow — create a new binding with the same name |
The rule is simple: := introduces, = updates. When in doubt, use := for new things and = for changing existing things.
Chapter 3: Types and Operators
Mog is statically typed with no implicit coercion. Every value has a known type at compile time, and the compiler will reject any operation that mixes types without an explicit conversion.
Scalar Types
Mog has four categories of scalar types: integers, floats, booleans, and strings.
Integers
The default integer type is int — a 64-bit signed integer. This is what you should use for virtually all integer work:
count := 42; // int (inferred)
count: int = 42; // int (explicit)
negative := -17; // int
big := 1_000_000; // int — underscores for readabilityInteger literals support multiple bases:
decimal := 255; // decimal
hex := 0xFF; // hexadecimal
binary := 0b11111111; // binary
with_sep := 1_000_000; // underscores ignored, just for readabilityAll four of these hold the same value: 255 (except with_sep, which is 1,000,000).
Explicit-Width Integers
When precision matters — tensor element types, bitwise operations, or interop with hardware — Mog provides explicit-width integers:
small: i32 = 42; // 32-bit signed
index: u32 = 0; // 32-bit unsigned
offset: u64 = 1024; // 64-bit unsignedThe commonly used integer types:
| Type | Width | Range |
|---|---|---|
int |
64-bit signed | -2^63 to 2^63 - 1 |
i32 |
32-bit signed | -2^31 to 2^31 - 1 |
u32 |
32-bit unsigned | 0 to 2^32 - 1 |
u64 |
64-bit unsigned | 0 to 2^64 - 1 |
Mog also supports
i8,i16,u8, andu16, but you’ll rarely need them outside of tensor element types. Useintunless you have a specific reason to reach for something else.
In practice, the explicit-width types exist primarily for tensors and low-level work:
// Scalar code: just use int
count := 0;
limit := 100;
// Tensor code: width matters
indices := tensor<i32>([1000]);
image := tensor<u8>([3, 224, 224]);Floats
The default float type is float — a 64-bit (double-precision) floating point number:
pi := 3.14159; // float (inferred)
pi: float = 3.14159; // float (explicit)
small := 1.0e-5; // scientific notation
half := .5; // leading dot is allowedFor single-precision, use f32:
ratio: f32 = 0.75;The commonly used float types:
| Type | Width | Use case |
|---|---|---|
float |
64-bit (double) | Default for all float math |
f32 |
32-bit (single) | Tensor element type, GPU work |
Mog also supports
f16andbf16(bfloat16) for ML tensor element types — see Chapter 15 for details on tensors. For scalar code, always usefloat.
// Scalar code: just use float
loss := 0.0;
learning_rate := 0.001;
// Tensor code: precision matters
weights := tensor<f16>([768, 768]);
gradients := tensor<f32>([768, 768]);Booleans
The bool type has two values: true and false.
active := true;
found := false;
is_valid: bool = true;Booleans are returned by comparison and logical operators:
fn main() -> int {
x := 42;
is_positive := x > 0; // true
is_even := (x % 2) == 0; // true
both := is_positive && is_even; // true
println(both);
return 0;
}There are no truthy/falsy values in Mog. Conditions must be actual bool values — you can’t use 0, "", or none as a boolean:
count := 0;
// if count { ... } // compile error — count is int, not bool
if count == 0 { ... } // correctNo implicit boolean conversions. This is deliberate — it catches a whole class of bugs at compile time. If you want a boolean, write a comparison.
Strings
Strings are UTF-8, immutable, and double-quoted only:
name := "Alice";
greeting := "hello, world";
empty := "";Strings support escape sequences:
| Escape | Meaning |
|---|---|
\n |
Newline |
\t |
Tab |
\\ |
Backslash |
\" |
Double quote |
newline := "line one\nline two";
tab := "col1\tcol2";
quote := "she said \"hello\"";
backslash := "C:\\Users\\alice";String interpolation uses f-strings — prefix the string with f and wrap expressions in {braces}:
name := "Alice";
age := 30;
greeting := f"hello {name}"; // "hello Alice"
info := f"{name} is {age} years old"; // "Alice is 30 years old"
math := f"2 + 2 = {2 + 2}"; // "2 + 2 = 4"Concatenation uses +:
first := "hello";
second := " world";
combined := first + second; // "hello world"For most cases, f-string interpolation is cleaner than concatenation — see the String Concatenation section at the end of this chapter.
Type Conversions
Mog has no implicit coercion. If you want to convert between types, you must be explicit.
The as Keyword
Use as to cast between numeric types:
// int to float
x := 42;
y := x as float; // 42.0
// float to int (truncates toward zero)
pi := 3.14;
n := pi as int; // 3
// int to narrower int
big := 1000;
small := big as i32;
// Between float widths
precise: float = 3.14159265358979;
approx := precise as f32;as works between any numeric types:
count := 42;
count_f := count as float; // 42.0
count_i32 := count as i32; // 42 (as 32-bit)
count_u64 := count as u64; // 42 (as unsigned 64-bit)Warning: Narrowing conversions can lose data. Casting a large
inttoi8will wrap on overflow — no runtime error, just silent truncation.
String Conversions
Use str() to convert any scalar to a string:
s1 := str(42); // "42"
s2 := str(3.14); // "3.14"
s3 := str(true); // "true"
s4 := str(false); // "false"To parse strings into numbers, use int_from_string() and parse_float(). These return Result because parsing can fail (see Chapter 10 for full coverage of error handling):
result := int_from_string("42");
match result {
ok(n) => println(f"parsed: {n}"),
err(msg) => println(f"failed: {msg}"),
}
pi := parse_float("3.14");
match pi {
ok(f) => println(f"got: {f}"),
err(msg) => println(f"bad float: {msg}"),
}Using the ? operator for concise error propagation:
fn parse_pair(a: string, b: string) -> Result<int> {
x := int_from_string(a)?;
y := int_from_string(b)?;
return ok(x + y);
}No Implicit Conversions
Mog will not silently convert between types. Every one of these is a compile error:
x := 42;
y := 3.14;
// z := x + y; // error: can't add int and float
z := x as float + y; // correct: explicit cast first
// flag := x; // error if flag is typed as bool
flag := x != 0; // correct: explicit comparisonThis strictness catches bugs early. If Mog rejects an expression, it’s telling you to think about what conversion you actually want.
Operators
Arithmetic Operators
Standard math operators work on numeric types. Both operands must be the same type:
fn main() -> int {
a := 10;
b := 3;
println(a + b); // 13 addition
println(a - b); // 7 subtraction
println(a * b); // 30 multiplication
println(a / b); // 3 integer division (truncates)
println(a % b); // 1 modulo (remainder)
return 0;
}With floats:
fn main() -> int {
a := 10.0;
b := 3.0;
println(a + b); // 13.0
println(a - b); // 7.0
println(a * b); // 30.0
println(a / b); // 3.3333333333333335
println(a % b); // 1.0
return 0;
}Integer division truncates toward zero:
fn main() -> int {
println(7 / 2); // 3
println(-7 / 2); // -3
println(7 / -2); // -3
return 0;
}Comparison Operators
Comparisons return bool. Both operands must be the same type:
fn main() -> int {
x := 10;
y := 20;
println(x == y); // false — equal
println(x != y); // true — not equal
println(x < y); // true — less than
println(x > y); // false — greater than
println(x <= y); // true — less or equal
println(x >= y); // false — greater or equal
return 0;
}Strings compare lexicographically:
fn main() -> int {
println("apple" < "banana"); // true
println("abc" == "abc"); // true
println("Abc" == "abc"); // false — case sensitive
return 0;
}Logical Operators
Logical operators work on bool values only:
fn main() -> int {
a := true;
b := false;
println(a && b); // false — logical AND
println(a || b); // true — logical OR
println(!a); // false — logical NOT
println(!b); // true
return 0;
}&& and || short-circuit: the right side is only evaluated if needed:
fn check(items: [int], index: int) -> bool {
// Safe: if index is out of bounds, the second condition is never evaluated
return (index < items.len) && (items[index] > 0);
}Combining conditions:
fn is_valid_age(age: int) -> bool {
return (age >= 0) && (age <= 150);
}
fn needs_review(score: int, flagged: bool) -> bool {
return (score < 50) || flagged;
}Bitwise Operators
Bitwise operators work on integer types:
fn main() -> int {
a := 0b1100; // 12
b := 0b1010; // 10
println(a & b); // 8 — bitwise AND
println(a | b); // 14 — bitwise OR
println(a ^ b); // 6 — bitwise XOR
println(a << 2); // 48 — left shift
println(a >> 1); // 6 — right shift
return 0;
}Common bitwise patterns:
fn main() -> int {
// Check if a number is even
n := 42;
is_even := (n & 1) == 0; // true
// Set a flag bit
flags := 0;
flags = flags | 0b0100; // set bit 2
// Clear a flag bit
flags = flags & 0b1011; // clear bit 2
// Toggle a flag bit
flags = flags ^ 0b0010; // toggle bit 1
return 0;
}String Concatenation
The + operator concatenates strings:
fn main() -> int {
greeting := "hello" + " " + "world";
println(greeting); // hello world
name := "Alice";
message := "hi, " + name + "!";
println(message); // hi, Alice!
return 0;
}For most cases, f-string interpolation is cleaner than concatenation:
// Concatenation
message := "User " + name + " scored " + str(score) + " points";
// Interpolation — prefer this
message := f"User {name} scored {score} points";Flat Operators (No Precedence)
Mog has no operator precedence. All binary operators are flat — the compiler does not silently reorder operations based on a precedence table. Instead, Mog enforces explicit grouping through two simple rules:
1. Associative operators can chain with themselves.
The operators +, *, and/&&, or/||, &, and | are associative, so
repeating the same one is unambiguous:
total := a + b + c; // OK — same operator throughout
mask := READ | WRITE | EXEC; // OK
all_ok := x && y && z; // OK2. Everything else requires parentheses.
- Different operators cannot mix. Use parentheses to show intent:
result := a + (b * c); // OK — parens make grouping explicit
result := a + b * c; // COMPILE ERROR — mixed + and *
check := (x > 0) && (y > 0); // OK
check := x > 0 && y > 0; // COMPILE ERROR — mixed > and &&
is_even := (n % 2) == 0; // OK
is_even := n % 2 == 0; // COMPILE ERROR — mixed % and ==- Non-associative operators cannot chain, even with themselves:
diff := (a - b) - c; // OK — parenthesized
diff := a - b - c; // COMPILE ERROR — - is non-associative
ratio := (a / b) / c; // OK
ratio := a / b / c; // COMPILE ERROR — / is non-associativeNon-associative operators: -, /, %, ==, !=, <, <=, >, >=, <<, >>, ^.
Why no precedence?
Precedence tables are a common source of bugs — especially when mixing arithmetic, comparison, and logical operators. Mog makes every grouping decision visible in the source code. The cost is a few extra parentheses; the benefit is that the code always means exactly what it says.
// Clear and correct
fahrenheit := ((celsius * 9) / 5) + 32;
in_range := (x >= low) && (x <= high);
masked := (flags & MASK) != 0;Common Patterns
Clamping a Value
fn clamp(value: int, low: int, high: int) -> int {
if value < low { return low; }
if value > high { return high; }
return value;
}
fn main() -> int {
score := 150;
clamped := clamp(score, 0, 100);
println(clamped); // 100
return 0;
}Safe Division
fn safe_divide(a: float, b: float) -> Result<float> {
if b == 0.0 {
return err("division by zero");
}
return ok(a / b);
}
fn main() -> int {
match safe_divide(10.0, 3.0) {
ok(result) => println(f"result: {result}"),
err(msg) => println(f"error: {msg}"),
}
return 0;
}Type-Aware Accumulation
fn average(numbers: [int]) -> float {
sum := 0;
for n in numbers {
sum = sum + n;
}
return (sum as float) / (numbers.len as float);
}
fn main() -> int {
scores := [85, 92, 78, 95, 88];
avg := average(scores);
println(f"average: {avg}"); // average: 87.6
return 0;
}Bitflag Permissions
fn main() -> int {
READ := 1; // 0b001
WRITE := 2; // 0b010
EXEC := 4; // 0b100
// Grant read and write
perms := READ | WRITE;
// Check permissions
can_read := (perms & READ) != 0; // true
can_exec := (perms & EXEC) != 0; // false
// Add execute permission
perms = perms | EXEC;
can_exec = (perms & EXEC) != 0; // true
println(f"read: {can_read}");
println(f"exec: {can_exec}");
return 0;
}Building Results with Type Conversion
fn format_percentage(value: int, total: int) -> string {
pct := (value as float / total as float) * 100.0;
return str(round(pct)) + "%";
}
fn main() -> int {
passed := 87;
total := 100;
println(format_percentage(passed, total)); // 87%
return 0;
}Summary
Mog’s type system is small and strict:
- Use
intandfloatfor almost everything. Reach fori32,u32,u64, orf32only when working with tensors or hardware interop. - No implicit coercion. Use
asfor numeric casts,str()for string conversion,int_from_string()andparse_float()for parsing. - Operators require matching types. Both sides of
+,*,==, etc. must be the same type. - Booleans are booleans. No truthy/falsy — use explicit comparisons.
- Operators are flat — no precedence. Different operators cannot mix without parentheses, and non-associative operators cannot chain. This eliminates an entire class of bugs.
Chapter 4: Control Flow
Mog’s control flow is familiar if you’ve used any C-family language: if/else, while, for, break, continue, and match. No surprises — but a few details matter, like braces being required, if working as an expression, and match handling Result and Optional patterns.
If/Else
The basic form: a condition, a block, and optional else if / else chains. Braces are always required. Parentheses around the condition are optional.
fn main() -> int {
x := 42;
if x > 0 {
println("positive");
} else if x == 0 {
println("zero");
} else {
println("negative");
}
return 0;
}Parentheses are allowed but not required — use them when they help readability:
fn main() -> int {
a := 10;
b := 20;
// Both of these are valid
if a > b {
println("a wins");
}
if (a + b) > 25 {
println("sum is large");
}
return 0;
}Nested Conditions
Chains of else if work exactly as you’d expect. For complex classification, they read top to bottom:
fn classify_temperature(temp: int) -> string {
if temp >= 100 {
return "boiling";
} else if temp >= 80 {
return "very hot";
} else if temp >= 60 {
return "warm";
} else if temp >= 40 {
return "cool";
} else if temp >= 20 {
return "cold";
} else {
return "freezing";
}
}
fn main() -> int {
println(classify_temperature(95)); // very hot
println(classify_temperature(55)); // warm
println(classify_temperature(-10)); // freezing
return 0;
}Nested if blocks inside other if blocks:
fn describe_number(n: int) -> string {
if n > 0 {
if (n % 2) == 0 {
return "positive even";
} else {
return "positive odd";
}
} else if n < 0 {
if (n % 2) == 0 {
return "negative even";
} else {
return "negative odd";
}
} else {
return "zero";
}
}
fn main() -> int {
println(describe_number(7)); // positive odd
println(describe_number(-4)); // negative even
println(describe_number(0)); // zero
return 0;
}If as Expression
if can return a value. The last expression in each branch becomes the result. When used this way, else is required — the compiler needs a value for every case:
fn main() -> int {
x := 42;
sign := if x > 0 { 1 } else if x < 0 { -1 } else { 0 };
println(f"sign of {x}: {sign}"); // sign of 42: 1
return 0;
}This works anywhere you need an expression:
fn abs(n: int) -> int {
return if n >= 0 { n } else { 0 - n };
}
fn max(a: int, b: int) -> int {
return if a > b { a } else { b };
}
fn min(a: int, b: int) -> int {
return if a < b { a } else { b };
}
fn main() -> int {
println(abs(-17)); // 17
println(max(10, 20)); // 20
println(min(10, 20)); // 10
return 0;
}If-expressions are useful for inline decisions without creating temporary variables:
fn format_count(n: int) -> string {
label := if n == 1 { "item" } else { "items" };
return f"{n} {label}";
}
fn main() -> int {
println(format_count(1)); // 1 item
println(format_count(5)); // 5 items
println(format_count(0)); // 0 items
return 0;
}Combining conditions with logical operators:
fn can_vote(age: int, is_citizen: bool) -> bool {
return (age >= 18) && is_citizen;
}
fn main() -> int {
age := 25;
citizen := true;
if can_vote(age, citizen) {
println("eligible to vote");
} else {
println("not eligible");
}
// Compound conditions
score := 85;
if score >= 90 {
println("A");
} else if (score >= 80) && (score < 90) {
println("B");
} else if (score >= 70) && (score < 80) {
println("C");
} else {
println("below C");
}
return 0;
}While Loops
while repeats a block as long as a condition is true:
fn main() -> int {
i := 0;
while i < 5 {
println(f"i = {i}");
i = i + 1;
}
return 0;
}Accumulator Pattern
The most common use of while is accumulating a result when the loop condition depends on something more complex than a simple range:
fn sum_1_to_n(n: int) -> int {
total := 0;
i := 1;
while i <= n {
total = total + i;
i = i + 1;
}
return total;
}
fn main() -> int {
println(f"sum 1..100 = {sum_1_to_n(100)}"); // sum 1..100 = 5050
return 0;
}Factorial with a while loop:
fn factorial(n: int) -> int {
result := 1;
i := 2;
while i <= n {
result = result * i;
i = i + 1;
}
return result;
}
fn main() -> int {
println(f"5! = {factorial(5)}"); // 5! = 120
println(f"10! = {factorial(10)}"); // 10! = 3628800
return 0;
}Convergence Loops
while is the right choice when you’re iterating until a condition is met, not over a known range:
fn collatz_steps(n: int) -> int {
steps := 0;
val := n;
while val != 1 {
if (val % 2) == 0 {
val = val / 2;
} else {
val = (val * 3) + 1;
}
steps = steps + 1;
}
return steps;
}
fn main() -> int {
println(f"collatz(27) = {collatz_steps(27)} steps"); // collatz(27) = 111 steps
println(f"collatz(1) = {collatz_steps(1)} steps"); // collatz(1) = 0 steps
return 0;
}Integer square root by repeated approximation:
fn isqrt(n: int) -> int {
if n <= 1 { return n; }
guess := n / 2;
while (guess * guess) > n {
guess = (guess + (n / guess)) / 2;
}
return guess;
}
fn main() -> int {
println(f"isqrt(100) = {isqrt(100)}"); // isqrt(100) = 10
println(f"isqrt(50) = {isqrt(50)}"); // isqrt(50) = 7
return 0;
}Infinite Loops
Use while true for loops that exit with break:
fn main() -> int {
sum := 0;
n := 1;
while true {
sum = sum + n;
if sum > 100 {
break;
}
n = n + 1;
}
println(f"stopped at n={n}, sum={sum}"); // stopped at n=14, sum=105
return 0;
}For Loops
Mog has two styles of for loop: for..to with an inclusive upper bound, and for..in which iterates over ranges, arrays, and maps.
For-To (Inclusive Range)
for..to counts from a start value to an end value, inclusive of both ends:
fn main() -> int {
for i := 1 to 5 {
println(f"i = {i}");
}
// prints: 1, 2, 3, 4, 5
return 0;
}The counter variable is scoped to the loop body — it doesn’t exist outside:
fn main() -> int {
for i := 1 to 10 {
println(i);
}
// i is not accessible here
return 0;
}Summing with for..to:
fn main() -> int {
total := 0;
for i := 1 to 100 {
total = total + i;
}
println(f"sum = {total}"); // sum = 5050
return 0;
}For-In Range (Exclusive End)
The .. range operator creates a half-open range — inclusive of the start, exclusive of the end:
fn main() -> int {
for i in 0..5 {
println(f"i = {i}");
}
// prints: 0, 1, 2, 3, 4
return 0;
}This is the natural choice for zero-based indexing:
fn main() -> int {
names := ["Alice", "Bob", "Charlie"];
for i in 0..names.len {
println(f"index {i}: {names[i]}");
}
return 0;
}Computing a power function:
fn power(base: int, exp: int) -> int {
result := 1;
for i in 0..exp {
result = result * base;
}
return result;
}
fn main() -> int {
println(f"2^10 = {power(2, 10)}"); // 2^10 = 1024
println(f"3^5 = {power(3, 5)}"); // 3^5 = 243
return 0;
}When to Use Which
| Style | Syntax | End Bound | Best For |
|---|---|---|---|
for..to |
for i := 1 to 10 |
Inclusive | Human-friendly ranges (“1 through 10”) |
for..in range |
for i in 0..10 |
Exclusive | Array indexing, zero-based iteration |
fn main() -> int {
// Print multiplication table for 7 — "1 through 10" is natural
for i := 1 to 10 {
println(f"7 x {i} = {7 * i}");
}
// Sum array elements — zero-based indexing is natural
values := [10, 20, 30, 40, 50];
total := 0;
for i in 0..values.len {
total = total + values[i];
}
println(f"total = {total}"); // total = 150
return 0;
}For-In Array
Iterating directly over array elements — no index needed:
fn main() -> int {
fruits := ["apple", "banana", "cherry", "date"];
for fruit in fruits {
println(f"I like {fruit}");
}
return 0;
}Summing, filtering, searching:
fn sum(numbers: [int]) -> int {
total := 0;
for n in numbers {
total = total + n;
}
return total;
}
fn contains(items: [string], target: string) -> bool {
for item in items {
if item == target {
return true;
}
}
return false;
}
fn main() -> int {
scores := [85, 92, 78, 95, 88];
println(f"sum = {sum(scores)}"); // sum = 438
colors := ["red", "green", "blue"];
println(contains(colors, "green")); // true
println(contains(colors, "pink")); // false
return 0;
}Collecting results into a new array:
fn filter_even(numbers: [int]) -> [int] {
result: [int] = [];
for n in numbers {
if n % 2 == 0 {
result.push(n);
}
}
return result;
}
fn main() -> int {
nums := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
evens := filter_even(nums);
for n in evens {
print_string(f"{n} ");
}
println(""); // 2 4 6 8 10
return 0;
}For-In with Index
When you need both the index and the value, add a second variable before the comma:
fn main() -> int {
names := ["Alice", "Bob", "Charlie", "Diana"];
for i, name in names {
println(f"{i}: {name}");
}
// 0: Alice
// 1: Bob
// 2: Charlie
// 3: Diana
return 0;
}This is cleaner than manually tracking an index counter:
fn find_index(items: [string], target: string) -> int {
for i, item in items {
if item == target {
return i;
}
}
return -1;
}
fn main() -> int {
colors := ["red", "green", "blue", "yellow"];
idx := find_index(colors, "blue");
println(f"blue is at index {idx}"); // blue is at index 2
idx2 := find_index(colors, "purple");
println(f"purple is at index {idx2}"); // purple is at index -1
return 0;
}Printing a numbered list:
fn main() -> int {
tasks := ["write code", "run tests", "fix bugs", "deploy"];
for i, task in tasks {
println(f" {i + 1}. {task}");
}
// 1. write code
// 2. run tests
// 3. fix bugs
// 4. deploy
return 0;
}Finding the maximum element and its position:
fn max_with_index(numbers: [int]) -> [int] {
best := numbers[0];
best_idx := 0;
for i, n in numbers {
if n > best {
best = n;
best_idx = i;
}
}
return [best_idx, best];
}
fn main() -> int {
scores := [72, 95, 88, 91, 67];
result := max_with_index(scores);
println(f"max value {result[1]} at index {result[0]}"); // max value 95 at index 1
return 0;
}For-In Map
Maps iterate as key-value pairs:
fn main() -> int {
scores := {"alice": 95, "bob": 87, "charlie": 92};
for name, score in scores {
println(f"{name} scored {score}");
}
return 0;
}Building a report from a map:
fn main() -> int {
inventory := {"apples": 12, "bananas": 5, "oranges": 8, "grapes": 0};
total := 0;
out_of_stock := 0;
for item, count in inventory {
total = total + count;
if count == 0 {
println(f" WARNING: {item} is out of stock");
out_of_stock = out_of_stock + 1;
}
}
println(f"total items: {total}");
println(f"out of stock: {out_of_stock}");
return 0;
}Transforming map data:
fn main() -> int {
temps_celsius := {"London": 15, "Tokyo": 28, "New York": 22, "Sydney": 19};
for city, celsius in temps_celsius {
fahrenheit := ((celsius * 9) / 5) + 32;
println(f"{city}: {celsius}C = {fahrenheit}F");
}
return 0;
}Counting occurrences with a map:
fn main() -> int {
words := ["the", "cat", "sat", "on", "the", "mat", "the", "cat"];
counts: {string: int} = {};
for word in words {
if counts[word] is some(n) {
counts[word] = n + 1;
} else {
counts[word] = 1;
}
}
for word, count in counts {
println(f"{word}: {count}");
}
return 0;
}Break and Continue
break exits the innermost enclosing loop immediately. continue skips the rest of the current iteration and moves to the next one.
Break
fn main() -> int {
// Find the first multiple of 7 greater than 50
for i in 1..100 {
if (i * 7) > 50 {
println(f"found: {i} (7 * {i} = {i * 7})");
break;
}
}
// found: 8 (7 * 8 = 56)
return 0;
}Continue
fn main() -> int {
// Print only odd numbers from 0 to 19
for i in 0..20 {
if (i % 2) == 0 {
continue;
}
print_string(f"{i} ");
}
println(""); // 1 3 5 7 9 11 13 15 17 19
return 0;
}Combined Break and Continue
fn main() -> int {
// Sum numbers from 1 to 100, skipping multiples of 3, stopping if sum exceeds 500
total := 0;
stopped_at := 0;
for i in 1..101 {
if (i % 3) == 0 {
continue;
}
total = total + i;
if total > 500 {
stopped_at = i;
break;
}
}
println(f"stopped at {stopped_at}, total = {total}");
return 0;
}Break and Continue in Nested Loops
break and continue affect only the innermost loop:
fn main() -> int {
// Find the first pair (i, j) where i * j == 42
found_i := 0;
found_j := 0;
done := false;
for i in 1..20 {
if done { break; }
for j in 1..20 {
if (i * j) == 42 {
found_i = i;
found_j = j;
done = true;
break; // breaks the inner loop
}
}
}
println(f"{found_i} * {found_j} = 42");
return 0;
}Skipping specific combinations in nested loops:
fn main() -> int {
// Print coordinate pairs, skip the diagonal where i == j
for i in 0..4 {
for j in 0..4 {
if i == j {
continue; // skips this iteration of the inner loop
}
print_string(f"({i},{j}) ");
}
}
println("");
return 0;
}A practical example — searching a 2D grid:
fn main() -> int {
grid := [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
];
target := 7;
found_row := -1;
found_col := -1;
for r in 0..3 {
for c in 0..4 {
if grid[r][c] == target {
found_row = r;
found_col = c;
break;
}
}
if found_row >= 0 {
break;
}
}
if found_row >= 0 {
println(f"found {target} at ({found_row}, {found_col})"); // found 7 at (1, 2)
} else {
println(f"{target} not found");
}
return 0;
}Match
match compares a value against a series of patterns and executes the first one that matches. Arms are separated by commas, and _ is the wildcard that matches anything.
Matching Integers
fn main() -> int {
day := 3;
match day {
1 => println("Monday"),
2 => println("Tuesday"),
3 => println("Wednesday"),
4 => println("Thursday"),
5 => println("Friday"),
6 => println("Saturday"),
7 => println("Sunday"),
_ => println("invalid day"),
}
return 0;
}Matching Strings
fn describe_color(color: string) -> string {
return match color {
"red" => "warm",
"orange" => "warm",
"yellow" => "warm",
"blue" => "cool",
"green" => "cool",
"purple" => "cool",
_ => "unknown",
};
}
fn main() -> int {
println(describe_color("red")); // warm
println(describe_color("blue")); // cool
println(describe_color("magenta")); // unknown
return 0;
}Multi-Statement Arms
When an arm needs more than one expression, wrap it in braces:
fn main() -> int {
code := 404;
match code {
200 => println("OK"),
301 => {
println("Moved Permanently");
println("Check the Location header");
},
404 => {
println("Not Found");
println("The resource does not exist");
},
500 => {
println("Internal Server Error");
println("Something went wrong on the server");
},
_ => println(f"HTTP {code}"),
}
return 0;
}Match as Expression
match returns a value, so you can assign its result:
fn main() -> int {
score := 85;
grade := match score / 10 {
10 => "A+",
9 => "A",
8 => "B",
7 => "C",
6 => "D",
_ => "F",
};
println(f"score {score} -> grade {grade}"); // score 85 -> grade B
return 0;
}Using match to drive computation:
fn fibonacci(n: int) -> int {
return match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
};
}
fn main() -> int {
for i in 0..10 {
print_string(f"{fibonacci(i)} ");
}
println(""); // 0 1 1 2 3 5 8 13 21 34
return 0;
}Assigning different values based on a key:
fn http_status_message(code: int) -> string {
return match code {
200 => "OK",
201 => "Created",
204 => "No Content",
301 => "Moved Permanently",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
502 => "Bad Gateway",
503 => "Service Unavailable",
_ => f"Unknown ({code})",
};
}
fn main() -> int {
codes := [200, 404, 500, 418];
for code in codes {
println(f"{code}: {http_status_message(code)}");
}
// 200: OK
// 404: Not Found
// 500: Internal Server Error
// 418: Unknown (418)
return 0;
}Matching on Result and Optional
Mog’s Result<T> and Optional (?T) types have variants that match can destructure. This is a brief preview — Chapter 10 covers error handling in depth.
Result patterns: ok(value) and err(message):
fn safe_divide(a: int, b: int) -> Result<int> {
if b == 0 {
return err("division by zero");
}
return ok(a / b);
}
fn main() -> int {
result := safe_divide(42, 6);
match result {
ok(value) => println(f"result: {value}"),
err(msg) => println(f"error: {msg}"),
}
result2 := safe_divide(10, 0);
match result2 {
ok(value) => println(f"result: {value}"),
err(msg) => println(f"error: {msg}"),
}
// result: 7
// error: division by zero
return 0;
}Optional patterns: some(value) and none:
fn find_positive(numbers: [int]) -> ?int {
for n in numbers {
if n > 0 {
return some(n);
}
}
return none;
}
fn main() -> int {
result := find_positive([-3, -1, 4, -2, 5]);
val := match result {
some(n) => n,
none => 0,
};
println(f"first positive: {val}"); // first positive: 4
result2 := find_positive([-3, -1, -2]);
val2 := match result2 {
some(n) => n,
none => 0,
};
println(f"first positive: {val2}"); // first positive: 0
return 0;
}Match on Result with multi-statement arms:
fn parse_and_double(input: string) -> Result<int> {
n := int_from_string(input)?;
return ok(n * 2);
}
fn main() -> int {
inputs := ["21", "abc", "50"];
for input in inputs {
match parse_and_double(input) {
ok(value) => {
println(f" '{input}' -> {value}");
},
err(msg) => {
println(f" '{input}' failed: {msg}");
},
}
}
// '21' -> 42
// 'abc' failed: invalid integer
// '50' -> 100
return 0;
}Practical Examples
Fibonacci (Iterative)
fn fibonacci(n: int) -> int {
if n <= 1 { return n; }
a := 0;
b := 1;
for i in 2..n+1 {
temp := a + b;
a = b;
b = temp;
}
return b;
}
fn main() -> int {
for i in 0..15 {
println(f"fib({i}) = {fibonacci(i)}");
}
return 0;
}Sum of Squares
fn sum_of_squares(n: int) -> int {
total := 0;
for i := 1 to n {
total = total + (i * i);
}
return total;
}
fn main() -> int {
println(f"sum of squares 1..10 = {sum_of_squares(10)}"); // 385
println(f"sum of squares 1..100 = {sum_of_squares(100)}"); // 338350
return 0;
}Linear Search
fn linear_search(items: [int], target: int) -> int {
for i, item in items {
if item == target {
return i;
}
}
return -1;
}
fn main() -> int {
data := [4, 8, 15, 16, 23, 42];
idx := linear_search(data, 23);
println(f"23 is at index {idx}"); // 23 is at index 4
idx2 := linear_search(data, 99);
println(f"99 is at index {idx2}"); // 99 is at index -1
return 0;
}Bubble Sort
fn bubble_sort(arr: [int]) -> [int] {
sorted := arr;
n := sorted.len;
for i in 0..n {
for j in 0..((n - i) - 1) {
if sorted[j] > sorted[j + 1] {
temp := sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = temp;
}
}
}
return sorted;
}
fn main() -> int {
data := [64, 34, 25, 12, 22, 11, 90];
sorted := bubble_sort(data);
for n in sorted {
print_string(f"{n} ");
}
println(""); // 11 12 22 25 34 64 90
return 0;
}FizzBuzz
fn main() -> int {
for i := 1 to 30 {
if (i % 15) == 0 {
println("FizzBuzz");
} else if (i % 3) == 0 {
println("Fizz");
} else if (i % 5) == 0 {
println("Buzz");
} else {
println(i);
}
}
return 0;
}GCD (Euclidean Algorithm)
fn gcd(a: int, b: int) -> int {
x := a;
y := b;
while y != 0 {
temp := y;
y = x % y;
x = temp;
}
return x;
}
fn main() -> int {
println(f"gcd(48, 18) = {gcd(48, 18)}"); // gcd(48, 18) = 6
println(f"gcd(100, 75) = {gcd(100, 75)}"); // gcd(100, 75) = 25
println(f"gcd(17, 13) = {gcd(17, 13)}"); // gcd(17, 13) = 1
return 0;
}Prime Checker
fn is_prime(n: int) -> bool {
if n < 2 { return false; }
if n < 4 { return true; }
if (n % 2) == 0 { return false; }
i := 3;
while (i * i) <= n {
if (n % i) == 0 {
return false;
}
i = i + 2;
}
return true;
}
fn main() -> int {
println("Primes up to 50:");
for n in 2..51 {
if is_prime(n) {
print_string(f"{n} ");
}
}
println(""); // 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
return 0;
}Command Dispatcher with Match
fn handle_command(cmd: string) -> string {
return match cmd {
"help" => "Available commands: help, version, quit",
"version" => "Mog v1.0.0",
"quit" => "Goodbye!",
_ => f"Unknown command: {cmd}",
};
}
fn main() -> int {
commands := ["help", "version", "status", "quit"];
for cmd in commands {
println(f"> {cmd}");
println(f" {handle_command(cmd)}");
}
return 0;
}Summary
| Construct | Syntax | Notes |
|---|---|---|
| If/else | if cond { } else { } |
Braces required, parens optional |
| If expression | x := if cond { a } else { b }; |
Returns last value of each branch |
| While | while cond { } |
Loop until condition is false |
| For-to | for i := 1 to 10 { } |
Inclusive upper bound |
| For-in range | for i in 0..10 { } |
Exclusive upper bound |
| For-in array | for item in arr { } |
Iterates values |
| For-in indexed | for i, item in arr { } |
Iterates index-value pairs |
| For-in map | for key, val in map { } |
Iterates key-value pairs |
| Break | break; |
Exits innermost loop |
| Continue | continue; |
Skips to next iteration |
| Match | match val { pat => expr, } |
Comma-separated arms, _ wildcard |
| Match expression | x := match val { ... }; |
Returns value from matched arm |
Chapter 5: Functions
Functions are the primary building blocks of any Mog program. They group reusable logic behind a name, accept typed parameters, and can return values. This chapter covers everything from basic declarations to recursion and the built-in functions that ship with every Mog program.
Basic Function Declarations
A function starts with the fn keyword, followed by a name, a parameter list with type annotations, an optional return type after ->, and a body in curly braces:
fn add(a: int, b: int) -> int {
return a + b;
}
fn multiply(x: float, y: float) -> float {
return x * y;
}
fn is_even(n: int) -> bool {
return (n % 2) == 0;
}Every function that produces a value must use an explicit return statement. Mog does not support implicit returns — the last expression in a function body is not automatically returned. This keeps control flow unambiguous.
// WRONG — this compiles but returns void, discarding the result
fn broken_add(a: int, b: int) -> int {
a + b;
}
// CORRECT
fn working_add(a: int, b: int) -> int {
return a + b;
}Always use
return. Unlike some languages where the last expression is the return value, Mog requires you to be explicit. This avoids subtle bugs when refactoring.
Multiple return points are fine when the logic calls for it:
fn classify_temperature(celsius: float) -> string {
if celsius < 0.0 {
return "freezing";
}
if celsius < 20.0 {
return "cold";
}
if celsius < 35.0 {
return "warm";
}
return "hot";
}Void Functions
When a function performs an action but doesn’t produce a value, omit the -> Type annotation. The function implicitly returns void:
fn log_message(level: string, message: string) {
println(f"[{level}] {message}");
}
fn swap(arr: [int], i: int, j: int) {
temp := arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
fn print_separator(width: int) {
line := "";
for _ in 0..width {
line = line + "-";
}
println(line);
}A void function can still use return; to exit early:
fn print_if_positive(n: int) {
if n <= 0 {
return;
}
println(n);
}Parameters and Return Types
Parameters are declared with name: Type syntax. Every parameter must have a type annotation — Mog does not infer parameter types (see Chapter 2 for why type annotations are required on function signatures).
fn format_price(amount: float, currency: string) -> string {
return f"{currency} {amount}";
}
fn clamp(value: int, low: int, high: int) -> int {
if value < low {
return low;
}
if value > high {
return high;
}
return value;
}Functions can accept and return composite types — arrays, maps, and structs:
fn sum(numbers: [int]) -> int {
total := 0;
for n in numbers {
total = total + n;
}
return total;
}
fn zip_names_ages(names: [string], ages: [int]) -> [{name: string, age: int}] {
result: [{name: string, age: int}] = [];
for i in 0..names.len {
result.push({name: names[i], age: ages[i]});
}
return result;
}Named Arguments and Default Values
Functions can declare default values for parameters. Callers may then omit those arguments or pass them by name in any order:
fn greet(name: string, greeting: string = "Hello") -> string {
return f"{greeting}, {name}!";
}
// All of these work:
greet("Alice"); // "Hello, Alice!"
greet("Bob", "Hey"); // "Hey, Bob!"
greet(name: "Charlie"); // "Hello, Charlie!"
greet(name: "Dave", greeting: "Hi"); // "Hi, Dave!"
greet(greeting: "Howdy", name: "Eve"); // "Howdy, Eve!"Named arguments are especially useful when a function has several optional parameters:
fn create_server(
host: string = "localhost",
port: int = 8080,
max_connections: int = 100,
timeout_ms: int = 30000,
) {
println(f"Starting server on {host}:{port}");
println(f"Max connections: {max_connections}");
println(f"Timeout: {timeout_ms}ms");
}
// Only override what you need:
create_server(); // all defaults
create_server(port: 3000); // just change port
create_server(host: "0.0.0.0", port: 443); // host and port
create_server(timeout_ms: 60000, port: 9090); // any orderA practical example — building a configurable search:
fn search(
query: string,
max_results: int = 10,
case_sensitive: bool = false,
sort_by: string = "relevance",
) -> [string] {
println(f"Searching for '{query}' (max={max_results}, case={case_sensitive}, sort={sort_by})");
results: [string] = [];
return results;
}
fn main() -> int {
search("mog language");
search("mog language", max_results: 50, sort_by: "date");
search(query: "Functions", case_sensitive: true);
return 0;
}Calling Conventions
Mog supports both positional and named calling styles. You can mix them, but positional arguments must come before named arguments:
fn send_email(to: string, subject: string, body: string = "", urgent: bool = false) {
println(f"To: {to}");
println(f"Subject: {subject}");
if urgent {
println("[URGENT]");
}
if body != "" {
println(body);
}
}
// Positional
send_email("alice@example.com", "Meeting", "See you at 3pm", true);
// Named
send_email(to: "bob@example.com", subject: "Lunch?");
// Mixed: positional first, then named
send_email("carol@example.com", "Report", urgent: true);Rule of thumb: Use positional args for 1-2 required parameters. Use named args when a function has 3+ parameters or when the call site would otherwise be ambiguous.
Recursion
Functions can call themselves. Mog does not guarantee tail-call optimization, so deep recursion will consume stack space proportional to the call depth.
Factorial:
fn factorial(n: int) -> int {
if n <= 1 {
return 1;
}
return n * factorial(n - 1);
}
fn main() -> int {
println(factorial(5)); // 120
println(factorial(10)); // 3628800
return 0;
}Fibonacci:
fn fibonacci(n: int) -> int {
if n <= 0 {
return 0;
}
if n == 1 {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
fn main() -> int {
for i in 0..10 {
println(fibonacci(i));
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
return 0;
}Binary search — recursion with arrays:
fn binary_search(arr: [int], target: int, low: int, high: int) -> int {
if low > high {
return -1;
}
mid := (low + high) / 2;
if arr[mid] == target {
return mid;
}
if arr[mid] < target {
return binary_search(arr, target, mid + 1, high);
}
return binary_search(arr, target, low, mid - 1);
}
fn main() -> int {
data := [2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
idx := binary_search(data, 23, 0, data.len - 1);
println(f"Found 23 at index {idx}"); // Found 23 at index 5
return 0;
}Greatest common divisor:
fn gcd(a: int, b: int) -> int {
if b == 0 {
return a;
}
return gcd(b, a % b);
}
fn main() -> int {
println(gcd(48, 18)); // 6
println(gcd(100, 75)); // 25
return 0;
}Tip: For deep or performance-sensitive recursion, consider rewriting with a loop (see Chapter 4). The recursive Fibonacci above is O(2^n) — the iterative version is O(n):
// Iterative fibonacci — much faster for large n
fn fibonacci_fast(n: int) -> int {
if n <= 0 {
return 0;
}
a := 0;
b := 1;
for _ in 1..n {
temp := b;
b = a + b;
a = temp;
}
return b;
}Math Builtins
Mog provides a set of math functions as builtins. No imports required. All math builtins operate on float (f64) values.
Single-argument functions:
| Function | Description |
|---|---|
sqrt(x) |
Square root |
sin(x) |
Sine (radians) |
cos(x) |
Cosine (radians) |
tan(x) |
Tangent (radians) |
asin(x) |
Arcsine |
acos(x) |
Arccosine |
exp(x) |
e^x |
log(x) |
Natural logarithm |
log2(x) |
Base-2 logarithm |
floor(x) |
Round down |
ceil(x) |
Round up |
round(x) |
Round to nearest |
abs(x) |
Absolute value |
Two-argument functions:
| Function | Description |
|---|---|
pow(x, y) |
x raised to the power y |
atan2(y, x) |
Two-argument arctangent |
min(a, b) |
Smaller of two values |
max(a, b) |
Larger of two values |
All math builtins take and return
float. If you have anint, cast it first withas float(see Chapter 3).
Examples:
// Distance between two points
fn distance(x1: float, y1: float, x2: float, y2: float) -> float {
dx := x2 - x1;
dy := y2 - y1;
return sqrt((dx * dx) + (dy * dy));
}
fn main() -> int {
println(distance(0.0, 0.0, 3.0, 4.0)); // 5.0
return 0;
}// Convert degrees to radians and compute trig values
fn deg_to_rad(degrees: float) -> float {
return (degrees * 3.14159265) / 180.0;
}
fn main() -> int {
angle := deg_to_rad(45.0);
println(sin(angle)); // ~0.7071
println(cos(angle)); // ~0.7071
return 0;
}// Compound interest
fn compound_interest(principal: float, rate: float, years: int) -> float {
return principal * pow(1.0 + rate, years as float);
}
fn main() -> int {
result := compound_interest(1000.0, 0.05, 10);
println(round(result)); // 1629.0
return 0;
}// Clamp a float between bounds
fn clamp_float(value: float, lo: float, hi: float) -> float {
return max(lo, min(hi, value));
}
fn main() -> int {
println(clamp_float(150.0, 0.0, 100.0)); // 100.0
println(clamp_float(-5.0, 0.0, 100.0)); // 0.0
return 0;
}// Estimate how many bits are needed to represent n
fn bits_needed(n: int) -> int {
if n <= 0 {
return 1;
}
return floor(log2(n as float)) as int + 1;
}
fn main() -> int {
println(bits_needed(255)); // 8
println(bits_needed(256)); // 9
return 0;
}Other Builtins
Mog provides several non-math builtins that are available without imports.
Output Functions
fn main() -> int {
// println auto-dispatches by type
println(42); // prints "42\n"
println(3.14); // prints "3.14\n"
println("hello"); // prints "hello\n"
println(true); // prints "true\n"
// Type-specific print (no newline)
print_string("name: ");
print_i64(42);
print_f64(3.14);
return 0;
}Conversion Functions
str(value) — Convert an int, float, or bool to a string:
s1 := str(42); // "42"
s2 := str(3.14); // "3.14"
s3 := str(true); // "true"
println("The answer is " + str(42));len(array) — Get the length of an array as an int:
numbers := [10, 20, 30, 40];
println(len(numbers)); // 4
empty: [string] = [];
println(len(empty)); // 0int_from_string(s) — Parse a string into an int. Returns Result<int> because the parse can fail (see Chapter 10 for full coverage of Result):
result := int_from_string("42");
match result {
ok(n) => println(f"Parsed: {n}"),
err(msg) => println(f"Failed: {msg}"),
}
// Using ? to propagate errors
fn read_port(input: string) -> Result<int> {
port := int_from_string(input)?;
if (port < 1) || (port > 65535) {
return err("port out of range");
}
return ok(port);
}parse_float(s) — Parse a string into a float. Returns Result<float>:
result := parse_float("3.14159");
match result {
ok(f) => println(f"Got pi: {f}"),
err(msg) => println(f"Not a float: {msg}"),
}
// Practical use: parsing user input
fn parse_temperature(input: string) -> Result<float> {
temp := parse_float(input)?;
if temp < -273.15 {
return err("below absolute zero");
}
return ok(temp);
}A Complete Example
Combining functions, conversions, and error handling:
fn parse_csv_row(line: string) -> Result<{name: string, age: int, score: float}> {
parts := line.split(",");
if len(parts) != 3 {
return err(f"expected 3 fields, got {len(parts)}");
}
age := int_from_string(parts[1])?;
score := parse_float(parts[2])?;
return ok({name: parts[0], age: age, score: score});
}
fn main() -> int {
row := parse_csv_row("Alice,30,95.5");
match row {
ok(data) => println(f"{data.name} is {str(data.age)} years old with score {str(data.score)}"),
err(msg) => println(f"Parse error: {msg}"),
}
return 0;
}Summary
| Feature | Syntax | Notes |
|---|---|---|
| Declaration | fn name(p: T) -> R { } |
fn keyword, typed params, explicit return type |
| Void function | fn name(p: T) { } |
Omit -> R for side-effect-only functions |
| Return | return value; |
Required — no implicit returns |
| Default args | fn f(x: int = 10) |
Caller can omit or pass by name |
| Named call | f(x: 42, y: 10) |
Named args can be in any order |
| Mixed call | f(42, y: 10) |
Positional before named |
| Recursion | fn f(n: int) { f(n-1); } |
No guaranteed tail-call optimization |
Chapter 6: Closures and Higher-Order Functions
In the previous chapter, functions were always named and declared at the top level. Mog also supports closures — anonymous functions that can be created inline, stored in variables, passed as arguments, and returned from other functions. When combined with higher-order functions (functions that accept or return other functions), closures unlock powerful and concise patterns for working with data.
Closure Syntax
A closure is an anonymous function written with the fn keyword but without a name. It is typically assigned to a variable or passed directly as an argument.
add := fn(a: int, b: int) -> int { return a + b; };
print(add(3, 4)); // 7The syntax mirrors named functions: parameters with types, an optional return type, and a body in curly braces. The trailing semicolon is required because the closure assignment is a statement.
square := fn(n: int) -> int { return n * n; };
is_positive := fn(x: float) -> bool { return x > 0.0; };
greet := fn(name: string) -> string { return "hello, {name}!"; };
print(square(5)); // 25
print(is_positive(-3.0)); // false
print(greet("Alice")); // hello, Alice!Closures that take no parameters and return nothing work too:
say_hi := fn() { print("hi"); };
say_hi(); // hiCapturing Variables
Closures can reference variables from their enclosing scope. This is what distinguishes a closure from a plain function pointer — it “closes over” the environment where it was created.
multiplier := 3;
triple := fn(n: int) -> int { return n * multiplier; };
print(triple(10)); // 30
print(triple(7)); // 21Closures can capture multiple variables:
base_url := "https://api.example.com";
api_key := "sk-12345";
make_url := fn(endpoint: string) -> string {
return "{base_url}/{endpoint}?key={api_key}";
};
print(make_url("users")); // https://api.example.com/users?key=sk-12345Value Capture Semantics
Mog captures variables by value — the closure gets a snapshot of each captured variable at the moment the closure is created. Later changes to the original do not affect the closure’s copy.
count := 10;
get_count := fn() -> int { return count; };
count = 20;
print(get_count()); // 10 — captured the value 10
print(count); // 20 — the original is 20This matters in loops. Each iteration creates a new closure that captures the loop variable’s current value:
makers: [fn() -> int] = [];
for i in 0..5 {
makers.push(fn() -> int { return i; });
}
print(makers[0]()); // 0
print(makers[3]()); // 3Internally, closures are implemented as a fat pointer:
{fn_ptr, env_ptr}. The runtime copies only the variables the closure actually references. This is an implementation detail you rarely need to think about.
Type Aliases for Function Types
Function type signatures can get verbose. Use type to create aliases:
type Predicate = fn(int) -> bool;
type Transform = fn(int) -> int;
type Callback = fn(string);These aliases simplify function signatures:
type Transform = fn(int) -> int;
fn apply_twice(f: Transform, value: int) -> int {
return f(f(value));
}
double := fn(n: int) -> int { return n * 2; };
print(apply_twice(double, 3)); // 12Without the alias, the signature would be fn apply_twice(f: fn(int) -> int, value: int) -> int — correct but harder to read at a glance.
type Predicate = fn(int) -> bool;
type Formatter = fn(int) -> string;
fn find_first(items: [int], pred: Predicate) -> ?int {
for item in items {
if pred(item) { return some(item); }
}
return none;
}
fn format_all(items: [int], fmt: Formatter) -> [string] {
return items.map(fmt);
}Passing Closures to Functions
Closures are first-class values. You can pass them as arguments using the function type syntax fn(ParamTypes) -> ReturnType.
fn apply(f: fn(int) -> int, x: int) -> int {
return f(x);
}
double := fn(n: int) -> int { return n * 2; };
negate := fn(n: int) -> int { return -n; };
print(apply(double, 5)); // 10
print(apply(negate, 5)); // -5You can pass closures inline without naming them:
print(apply(fn(n: int) -> int { return n * n; }, 4)); // 16A function that transforms every element of an array:
fn transform(arr: [int], f: fn(int) -> int) -> [int] {
result: [int] = [];
for item in arr {
result.push(f(item));
}
return result;
}
numbers := [1, 2, 3, 4, 5];
doubled := transform(numbers, fn(n: int) -> int { return n * 2; });
print(doubled); // [2, 4, 6, 8, 10]
offset := 100;
shifted := transform(numbers, fn(n: int) -> int { return n + offset; });
print(shifted); // [101, 102, 103, 104, 105]Returning Closures from Functions
Functions can create and return closures. The returned closure retains access to any variables it captured — even after the enclosing function has returned.
fn make_adder(n: int) -> fn(int) -> int {
return fn(x: int) -> int { return x + n; };
}
add5 := make_adder(5);
add100 := make_adder(100);
print(add5(3)); // 8
print(add100(3)); // 103fn make_multiplier(factor: float) -> fn(float) -> float {
return fn(x: float) -> float { return x * factor; };
}
to_km := make_multiplier(1.60934);
print(to_km(10.0)); // 16.0934This factory pattern is useful for creating families of related functions from a single template. You will see it again in Chapter 8 when we build constructor functions for structs.
Closures with Array Methods
Mog arrays have built-in methods — filter, map, and sort — that accept closures. These methods return new arrays; they do not modify the original. See Chapter 9 for the full set of collection operations.
filter
filter takes a predicate closure and returns a new array containing only the elements for which the predicate returns true.
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
evens := numbers.filter(fn(n: int) -> bool { return (n % 2) == 0; });
print(evens); // [2, 4, 6, 8, 10]
big := numbers.filter(fn(n: int) -> bool { return n > 7; });
print(big); // [8, 9, 10]Filter with a captured threshold:
scores := [45, 72, 88, 91, 53, 67, 79, 95];
cutoff := 70;
passing := scores.filter(fn(s: int) -> bool { return s >= cutoff; });
print(passing); // [72, 88, 91, 79, 95]map
map takes a transform closure and returns a new array with each element replaced by the closure’s result.
numbers := [1, 2, 3, 4, 5];
doubled := numbers.map(fn(n: int) -> int { return n * 2; });
print(doubled); // [2, 4, 6, 8, 10]
labels := numbers.map(fn(n: int) -> string { return "item-{str(n)}"; });
print(labels); // ["item-1", "item-2", "item-3", "item-4", "item-5"]names := ["alice", "bob", "carol"];
lengths := names.map(fn(name: string) -> int { return name.len; });
print(lengths); // [5, 3, 5]sort
sort takes a comparator closure that returns true when the first argument should come before the second. It returns a new sorted array.
numbers := [5, 2, 8, 1, 9, 3];
ascending := numbers.sort(fn(a: int, b: int) -> bool { return a < b; });
print(ascending); // [1, 2, 3, 5, 8, 9]
descending := numbers.sort(fn(a: int, b: int) -> bool { return a > b; });
print(descending); // [9, 8, 5, 3, 2, 1]Sorting structs by a specific field:
struct Player {
name: string,
score: int,
}
players := [
Player{name: "Alice", score: 250},
Player{name: "Bob", score: 180},
Player{name: "Carol", score: 320},
];
by_score := players.sort(fn(a: Player, b: Player) -> bool {
return a.score > b.score;
});
for p in by_score {
print("{p.name}: {str(p.score)}");
}
// Carol: 320
// Alice: 250
// Bob: 180Chaining Methods
Filter, map, and sort can be chained for expressive data pipelines:
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
result := numbers
.filter(fn(n: int) -> bool { return (n % 2) == 0; })
.map(fn(n: int) -> int { return n * n; })
.sort(fn(a: int, b: int) -> bool { return a > b; });
print(result); // [100, 64, 36, 16, 4]Method chaining reads top-to-bottom: filter the evens, square them, sort descending. Each step returns a new array, so the original
numbersis untouched.
Summary
| Concept | Syntax |
|---|---|
| Create a closure | fn(params) -> Type { body } |
| Type alias | type Name = fn(ParamTypes) -> ReturnType; |
| Pass to function | fn do_it(f: fn(int) -> int) { ... } |
| Return from function | fn make() -> fn(int) -> int { ... } |
| Filter an array | arr.filter(fn(x: T) -> bool { ... }) |
| Map an array | arr.map(fn(x: T) -> U { ... }) |
| Sort an array | arr.sort(fn(a: T, b: T) -> bool { ... }) |
Closures capture by value, are first-class values, and combine naturally with array methods for concise data processing. In the next chapter, we will look at Mog’s string type in detail.
Chapter 7: Strings
Strings are one of the most frequently used types in any language. Mog strings are immutable, UTF-8 encoded, and garbage-collected — you create them, pass them around, and the runtime handles the rest. This chapter covers everything from basic literals and escape sequences to interpolation, methods, and parsing.
String Basics
Mog strings use double quotes only. There are no single-quoted strings, no raw strings, and no heredocs. A string literal is a sequence of bytes between " and ", encoded as UTF-8 and null-terminated internally.
fn main() -> int {
greeting := "Hello, world!";
println(greeting);
empty := "";
println(empty); // prints an empty line
return 0;
}Escape Sequences
Mog supports the standard escape sequences you’d expect:
| Escape | Meaning |
|---|---|
\n |
Newline |
\t |
Tab |
\\ |
Literal backslash |
\" |
Literal double quote |
fn main() -> int {
multiline := "Line 1\nLine 2\nLine 3";
println(multiline);
// Line 1
// Line 2
// Line 3
tabbed := "Name:\tAlice\nAge:\t30";
println(tabbed);
// Name: Alice
// Age: 30
path := "C:\\Users\\alice\\docs";
println(path); // C:\Users\alice\docs
quoted := "She said \"hello\" and left.";
println(quoted); // She said "hello" and left.
return 0;
}Immutability
Strings in Mog are immutable reference types. You can rebind a variable to a new string, but you cannot modify a string’s contents in place. Every operation that “changes” a string — concatenation, upper(), replace() — returns a new string.
fn main() -> int {
s := "hello";
s = s.upper(); // rebinds s to a new string "HELLO"
println(s); // HELLO
return 0;
}UTF-8 Encoding
Mog strings are UTF-8 encoded, so they handle international text and emoji naturally. Keep in mind that .len returns the byte count, not the number of characters — multibyte characters take more than one byte.
fn main() -> int {
cafe := "café au lait";
println(cafe);
emoji := "Hello! 👍";
println(emoji);
chinese := "你好世界";
println(chinese);
japanese := "こんにちは";
println(japanese);
// Byte lengths — not character counts
ascii := "hello";
println(ascii.len); // 5 — one byte per character
return 0;
}String Interpolation
Mog supports f-strings — string literals prefixed with f that can embed expressions inside {} braces. This is the most readable way to build strings from mixed data.
fn main() -> int {
name := "Alice";
age := 30;
println(f"Hello, {name}!"); // Hello, Alice!
println(f"You are {age} years old."); // You are 30 years old.
return 0;
}Expressions inside {} can be arithmetic, function calls, or any expression that produces a value. The result is automatically converted to a string.
fn main() -> int {
age := 30;
println(f"Next year you'll be {age + 1}."); // Next year you'll be 31.
x := 7;
println(f"{x} squared is {x * x}."); // 7 squared is 49.
price := 9.99;
qty := 3;
println(f"Total: {price * 3.0}"); // Total: 29.970000
return 0;
}You can have multiple interpolations in a single string, and they can appear anywhere — at the start, middle, or end.
fn main() -> int {
a := 10;
b := 20;
println(f"{a} + {b} = {a + b}"); // 10 + 20 = 30
name := "World";
println(f"[{name}]"); // [World]
return 0;
}If you need a literal { or } in an f-string, escape it by doubling: {{ and }}.
fn main() -> int {
x := 42;
println(f"The value is: {x} (in braces: {{{x}}})");
// The value is: 42 (in braces: {42})
return 0;
}String Concatenation
The + operator joins two strings together, returning a new string. You can chain multiple + operations.
fn main() -> int {
first := "Hello";
second := " World";
result := first + second;
println(result); // Hello World
full := "one" + " " + "two" + " " + "three";
println(full); // one two three
return 0;
}To concatenate a non-string value, convert it first with str().
fn main() -> int {
count := 42;
msg := "Count: " + str(count);
println(msg); // Count: 42
pi := 3.14;
info := "Pi is approximately " + str(pi);
println(info); // Pi is approximately 3.140000
return 0;
}For most cases, f-strings are cleaner than manual concatenation — they handle the conversion automatically and are easier to read.
fn main() -> int {
name := "Alice";
score := 95;
// Concatenation — works, but verbose
msg1 := name + " scored " + str(score) + " points.";
// F-string — same result, easier to read
msg2 := f"{name} scored {score} points.";
println(msg1); // Alice scored 95 points.
println(msg2); // Alice scored 95 points.
return 0;
}String Methods
Mog strings have built-in methods for common operations. These are called with dot syntax on any string value.
.len
Returns the byte length of the string. This is a property, not a function call.
fn main() -> int {
s := "hello";
println(s.len); // 5
empty := "";
println(empty.len); // 0
// Multibyte characters take more than one byte
accent := "café";
println(accent.len); // 5 — the é is 2 bytes
return 0;
}.contains(substr)
Returns true if the string contains the given substring.
fn main() -> int {
s := "hello world";
if s.contains("world") {
println("Found 'world'");
}
if s.contains("xyz") == false {
println("No 'xyz' here");
}
// Case-sensitive
if s.contains("Hello") == false {
println("'Hello' not found — case matters");
}
return 0;
}.starts_with(prefix) and .ends_with(suffix)
Check whether a string begins or ends with a given substring.
fn main() -> int {
path := "/usr/local/bin/mog";
if path.starts_with("/usr") {
println("System path"); // prints
}
if path.ends_with(".mog") == false {
println("Not a .mog file"); // prints
}
filename := "report.csv";
if filename.ends_with(".csv") {
println("CSV file detected"); // prints
}
return 0;
}.upper() and .lower()
Return a new string with all ASCII characters converted to uppercase or lowercase.
fn main() -> int {
s := "Hello World";
println(s.upper()); // HELLO WORLD
println(s.lower()); // hello world
println(s); // Hello World — original unchanged
// Useful for case-insensitive comparison
input := "Yes";
if input.lower() == "yes" {
println("Confirmed");
}
return 0;
}.trim()
Returns a new string with leading and trailing whitespace removed.
fn main() -> int {
raw := " hello ";
cleaned := raw.trim();
println(cleaned); // hello
println(cleaned.len); // 5
padded := "\t spaced \n";
println(padded.trim()); // spaced
return 0;
}.replace(old, new)
Returns a new string with all occurrences of old replaced by new.
fn main() -> int {
s := "hello world";
println(s.replace("world", "Mog")); // hello Mog
csv := "a,b,c,d";
println(csv.replace(",", " | ")); // a | b | c | d
// Replaces all occurrences, not just the first
repeated := "aaa";
println(repeated.replace("a", "bb")); // bbbbbb
return 0;
}.split(delimiter)
Splits a string into an array of substrings at each occurrence of the delimiter.
fn main() -> int {
csv := "alice,bob,carol";
parts := csv.split(",");
for i, name in parts {
println(f"{i}: {name}");
}
// 0: alice
// 1: bob
// 2: carol
return 0;
}.index_of(substr)
Returns the byte offset of the first occurrence of substr, or -1 if not found.
fn main() -> int {
s := "hello world";
println(s.index_of("world")); // 6
println(s.index_of("xyz")); // -1
return 0;
}Method Chaining
String methods return new strings, so you can chain them.
fn main() -> int {
raw := " Hello, World! ";
result := raw.trim().lower().replace("world", "mog");
println(result); // hello, mog!
return 0;
}String Slicing
You can extract a substring using bracket syntax with a range: s[start:end]. Both start and end are byte offsets. The slice includes start and excludes end.
fn main() -> int {
s := "hello world";
println(s[0:5]); // hello
println(s[6:11]); // world
// Single character access
println(s[0]); // h
println(s[4]); // o
// Using variables
start := 6;
end := 11;
println(s[start:end]); // world
return 0;
}String Comparison
Use == and != to compare string contents. These compare the actual bytes, not pointer identity.
fn main() -> int {
a := "hello";
b := "hello";
c := "world";
if a == b {
println("a and b are equal"); // prints
}
if a != c {
println("a and c are different"); // prints
}
return 0;
}Comparisons are case-sensitive. Use .lower() or .upper() if you need case-insensitive matching.
fn main() -> int {
input := "YES";
expected := "yes";
if input == expected {
println("exact match");
}
if input.lower() == expected {
println("case-insensitive match"); // prints
}
return 0;
}Conversions
To String: str()
The str() function converts integers and floats to their string representation.
fn main() -> int {
s1 := str(42);
println(s1); // 42
s2 := str(-7);
println(s2); // -7
s3 := str(3.14);
println(s3); // 3.140000
// Useful for concatenation
label := "Score: " + str(100);
println(label); // Score: 100
return 0;
}From String: Parsing
Mog provides two sets of parsing functions with different error-handling strategies.
Safe parsing — int_from_string() and float_from_string() return a Result type that you can match on for error handling (see Chapter 10 for details on Result types):
fn main() -> int {
r := int_from_string("42");
// r is a Result<int> — use match to handle success or failure
return 0;
}Simple parsing — parse_int() and parse_float() return the value directly, giving 0 or 0.0 on failure:
fn main() -> int {
n := parse_int("123");
println(n); // 123
f := parse_float("3.14");
println(f); // 3.140000
// Invalid input returns 0
bad := parse_int("abc");
println(bad); // 0
return 0;
}Use parse_int and parse_float when you trust the input or have already validated it. Use int_from_string and float_from_string when you need to handle errors explicitly.
Print Functions
Mog provides generic print and println functions that automatically dispatch based on the argument type. There are also type-specific variants when you need precise control.
Generic Printing
println() detects the argument type and calls the appropriate variant:
fn main() -> int {
println("hello"); // dispatches to println_string
println(42); // dispatches to println_i64
println(3.14); // dispatches to println_f64
return 0;
}Type-Specific Variants
These print without a trailing newline, which is useful for building output piece by piece:
| Function | Description |
|---|---|
print_string(s) |
Print a string, no newline |
print_i64(n) |
Print an integer, no newline |
print_f64(f) |
Print a float, no newline |
println_string(s) |
Print a string with newline |
println_i64(n) |
Print an integer with newline |
println_f64(f) |
Print a float with newline |
fn main() -> int {
print_string("Loading");
print_string(".");
print_string(".");
print_string(".");
println_string(" done!");
// Loading... done!
print_string("Value: ");
print_i64(42);
print_string("\n");
// Value: 42
return 0;
}Building Strings
Concatenation in Loops
You can build a string incrementally by concatenating in a loop. Since strings are immutable, each + creates a new string.
fn main() -> int {
arr := [1, 2, 3, 4, 5];
result := "";
for i, v in arr {
if i > 0 {
result = result + ", ";
}
result = result + str(v);
}
println(result); // 1, 2, 3, 4, 5
return 0;
}Formatting Tables
F-strings and concatenation let you format aligned output:
fn main() -> int {
println("Name Score");
println("---- -----");
println(f"Alice {95}");
println(f"Bob {87}");
println(f"Carol {92}");
return 0;
}Building Messages
F-strings shine when assembling messages with mixed data:
fn main() -> int {
user := "alice";
action := "login";
code := 200;
log_msg := f"[{code}] User '{user}' performed '{action}'";
println(log_msg);
// [200] User 'alice' performed 'login'
items := 3;
total := 29.97;
receipt := f"You purchased {items} items for a total of {total}";
println(receipt);
return 0;
}Parsing with Validation
A common pattern is parsing user input and handling the case where it might not be valid:
fn main() -> int {
inputs := ["42", "hello", "100", "3.14"];
for i, s in inputs {
n := parse_int(s);
if n != 0 {
println(f"Parsed '{s}' as {n}");
} else {
if s == "0" {
println(f"Parsed '{s}' as 0");
} else {
println(f"Could not parse '{s}'");
}
}
}
return 0;
}Summary
| Operation | Syntax | Returns |
|---|---|---|
| Create string | "hello" |
string |
| Interpolation | f"value is {x}" |
string |
| Concatenation | a + b |
string |
| Byte length | s.len |
int |
| Contains | s.contains("x") |
bool |
| Starts with | s.starts_with("x") |
bool |
| Ends with | s.ends_with("x") |
bool |
| Uppercase | s.upper() |
string |
| Lowercase | s.lower() |
string |
| Trim whitespace | s.trim() |
string |
| Replace | s.replace("a", "b") |
string |
| Split | s.split(",") |
[string] |
| Index of | s.index_of("x") |
int |
| Slice | s[0:5] |
string |
| Char at | s[0] |
string |
| To string | str(42) |
string |
| Parse int (safe) | int_from_string("42") |
Result<int> |
| Parse float (safe) | float_from_string("3.14") |
Result<float> |
| Parse int (simple) | parse_int("42") |
int |
| Parse float (simple) | parse_float("3.14") |
float |
| Equality | a == b, a != b |
bool |
Strings are straightforward in Mog — double-quoted, immutable, UTF-8, and garbage-collected. For error handling with int_from_string and float_from_string, see Chapter 10 on Result types.
Chapter 8: Structs
Structs are Mog’s way of grouping related data under a single name. They are simple named product types with typed fields — no methods, no inheritance, no interfaces. You define the shape, construct instances, and pass them around. Functions that operate on structs live outside the struct as standalone functions.
Declaring Structs
A struct declaration lists named fields with their types, separated by commas:
struct Point {
x: int,
y: int,
}Fields can be any type — scalars, strings, arrays, maps, or other structs:
struct Color {
r: int,
g: int,
b: int,
a: float,
}
struct Config {
name: string,
version: int,
debug: bool,
tags: [string],
}
struct User {
id: int,
username: string,
email: string,
active: bool,
}Structs are always declared at the top level. They cannot be declared inside functions or other structs.
Constructing Instances
Create a struct instance by naming the type and providing values for all fields inside braces:
fn main() {
p := Point { x: 10, y: 20 };
c := Color { r: 255, g: 128, b: 0, a: 1.0 };
cfg := Config { name: "myapp", version: 3, debug: false, tags: ["prod", "v3"] };
}Every field must be provided. There are no default values and no partial construction — if a struct has four fields, you supply four values:
fn main() {
// This is a compile error — missing field `a`:
// c := Color { r: 255, g: 128, b: 0 };
// All fields required:
c := Color { r: 255, g: 128, b: 0, a: 1.0 };
}You can use expressions as field values, not just literals:
fn main() {
base := 100;
p := Point { x: base * 2, y: base + 50 };
print(p.x); // 200
print(p.y); // 150
}Field Access
Access individual fields with dot notation:
fn main() {
p := Point { x: 10, y: 20 };
print(p.x); // 10
print(p.y); // 20
c := Color { r: 255, g: 128, b: 0, a: 1.0 };
print(c.r); // 255
print(c.a); // 1.0
}Fields work anywhere an expression of that type is expected:
fn main() {
p := Point { x: 3, y: 4 };
distance_squared := (p.x * p.x) + (p.y * p.y);
print(distance_squared); // 25
}Field Mutation
Struct fields are mutable. Assign to them with =:
fn main() {
p := Point { x: 10, y: 20 };
print(p.x); // 10
p.x = 30;
print(p.x); // 30
p.y = p.y + 5;
print(p.y); // 25
}You can mutate any field at any time:
fn main() {
user := User { id: 1, username: "alice", email: "alice@example.com", active: true };
print(user.active); // true
user.active = false;
user.email = "alice@newdomain.com";
print(user.active); // false
print(user.email); // alice@newdomain.com
}Passing Structs to Functions
Structs are heap-allocated and passed by reference. When you pass a struct to a function, the function receives a pointer to the same data. Modifications inside the function affect the original:
fn move_right(p: Point, amount: int) {
p.x = p.x + amount;
}
fn main() {
p := Point { x: 0, y: 0 };
move_right(p, 10);
print(p.x); // 10 — the original was modified
}This is different from closures, which capture variables by value (see Chapter 6). Structs are always passed by reference — there is no copy-on-pass.
Functions can read struct fields without modifying them:
struct Rect {
width: int,
height: int,
}
fn area(r: Rect) -> int {
return r.width * r.height;
}
fn main() {
r := Rect { width: 10, height: 5 };
print(area(r)); // 50
}Returning structs from functions works naturally — you return a reference to a heap-allocated struct:
fn make_point(x: int, y: int) -> Point {
return Point { x: x, y: y };
}
fn main() {
p := make_point(3, 7);
print(p.x); // 3
print(p.y); // 7
}No Methods — Use Standalone Functions
Mog structs have no methods. Instead, write standalone functions that take the struct as a parameter. This keeps data and behavior separate:
struct Vec2 {
x: int,
y: int,
}
fn vec2_add(a: Vec2, b: Vec2) -> Vec2 {
return Vec2 { x: a.x + b.x, y: a.y + b.y };
}
fn vec2_dot(a: Vec2, b: Vec2) -> int {
return (a.x * b.x) + (a.y * b.y);
}
fn vec2_to_string(v: Vec2) -> string {
return "({v.x}, {v.y})";
}
fn main() {
a := Vec2 { x: 1, y: 2 };
b := Vec2 { x: 3, y: 4 };
sum := vec2_add(a, b);
print(vec2_to_string(sum)); // (4, 6)
print(vec2_dot(a, b)); // 11
}A common convention is to prefix function names with the struct name:
point_distance,color_mix,user_validate. This makes it clear which type the function operates on.
Constructor Functions
Since there are no constructors or default values, a common pattern is to write factory functions that return pre-configured struct instances:
fn new_user(name: string, email: string) -> User {
return User { id: 0, username: name, email: email, active: true };
}
fn main() {
u := new_user("alice", "alice@example.com");
print(u.username); // alice
print(u.active); // true
}struct DatabaseConfig {
host: string,
port: int,
name: string,
}
fn default_db_config() -> DatabaseConfig {
return DatabaseConfig { host: "localhost", port: 5432, name: "appdb" };
}
fn main() {
cfg := default_db_config();
cfg.name = "testdb";
print(cfg.host); // localhost
print(cfg.name); // testdb
}This pattern gives you the flexibility of default values while keeping construction explicit. See Chapter 6 for how closures can create factory functions that return configured behavior.
Nested Structs
Structs can contain other structs as fields:
struct Address {
street: string,
city: string,
zip: string,
}
struct Person {
name: string,
age: int,
address: Address,
}
fn main() {
p := Person {
name: "Alice",
age: 30,
address: Address { street: "123 Main St", city: "Portland", zip: "97201" },
};
print(p.name); // Alice
print(p.address.city); // Portland
print(p.address.zip); // 97201
}Mutation works through nested field access:
fn main() {
p := Person {
name: "Bob",
age: 25,
address: Address { street: "456 Oak Ave", city: "Seattle", zip: "98101" },
};
p.address.city = "Tacoma";
p.age = 26;
print(p.address.city); // Tacoma
}Since structs are passed by reference, modifying a nested struct through a function affects the original all the way up:
fn relocate(person: Person, new_city: string) {
person.address.city = new_city;
}
fn main() {
p := Person {
name: "Dana",
age: 35,
address: Address { street: "100 Elm St", city: "Austin", zip: "73301" },
};
relocate(p, "Houston");
print(p.address.city); // Houston
}Structs with Arrays and Maps
Struct fields can hold arrays and maps, enabling rich data models:
struct StudentRecord {
name: string,
grades: [int],
}
fn average_grade(s: StudentRecord) -> float {
sum := 0;
for g in s.grades {
sum = sum + g;
}
return (sum as float) / (s.grades.len as float);
}
fn main() {
student := StudentRecord { name: "Eve", grades: [88, 92, 75, 96] };
print(average_grade(student)); // 87.75
student.grades.push(100);
print(student.grades.len); // 5
}struct Inventory {
items: map[string]int,
}
fn add_item(inv: Inventory, name: string, qty: int) {
if inv.items.has(name) {
inv.items[name] = inv.items[name] + qty;
} else {
inv.items[name] = qty;
}
}
fn main() {
inv := Inventory { items: {} };
add_item(inv, "apples", 5);
add_item(inv, "bananas", 3);
add_item(inv, "apples", 2);
print(inv.items["apples"]); // 7
}See Chapter 9 for the full set of array and map operations.
Practical Examples
RGB Color Manipulation
struct Color {
r: int,
g: int,
b: int,
}
fn clamp(val: int, lo: int, hi: int) -> int {
if val < lo { return lo; }
if val > hi { return hi; }
return val;
}
fn brighten(c: Color, amount: int) {
c.r = clamp(c.r + amount, 0, 255);
c.g = clamp(c.g + amount, 0, 255);
c.b = clamp(c.b + amount, 0, 255);
}
fn mix(a: Color, b: Color) -> Color {
return Color {
r: (a.r + b.r) / 2,
g: (a.g + b.g) / 2,
b: (a.b + b.b) / 2,
};
}
fn color_to_string(c: Color) -> string {
return "rgb({c.r}, {c.g}, {c.b})";
}
fn main() {
red := Color { r: 200, g: 50, b: 50 };
blue := Color { r: 50, g: 50, b: 200 };
brighten(red, 40);
print(color_to_string(red)); // rgb(240, 90, 90)
purple := mix(red, blue);
print(color_to_string(purple)); // rgb(145, 70, 145)
}Tree Structure
Structs that contain arrays of the same type enable tree-like patterns:
struct TreeNode {
value: int,
children: [TreeNode],
}
fn sum_tree(node: TreeNode) -> int {
total := node.value;
for child in node.children {
total = total + sum_tree(child);
}
return total;
}
fn main() {
tree := TreeNode {
value: 1,
children: [
TreeNode { value: 2, children: [] },
TreeNode {
value: 3,
children: [
TreeNode { value: 4, children: [] },
TreeNode { value: 5, children: [] },
],
},
],
};
print(sum_tree(tree)); // 15
}Summary
| Concept | Syntax |
|---|---|
| Declare a struct | struct Name { field: type, ... } |
| Construct an instance | Name { field: value, ... } |
| Read a field | instance.field |
| Mutate a field | instance.field = value; |
| Nested field access | instance.field.subfield |
Structs are heap-allocated and passed by reference. There are no methods — use standalone functions that take the struct as a parameter. Keep structs simple: they hold data, functions provide behavior.
Chapter 9: Collections
Mog provides three collection types: arrays for ordered sequences, maps for key-value lookup, and SoA (Struct of Arrays) for cache-friendly columnar storage. Together they cover the vast majority of data organization needs.
Arrays
Arrays are dynamically-sized, ordered, homogeneous sequences. They grow and shrink as needed and are the most common collection type.
Array Literals
Create arrays with bracket syntax. The element type is inferred:
fn main() -> int {
numbers := [1, 2, 3, 4, 5];
names := ["Alice", "Bob", "Charlie"];
flags := [true, false, true];
println(numbers.len()); // 5
println(names.len()); // 3
return 0;
}Repeat Syntax
Create arrays filled with a repeated value using [value; count]:
fn main() -> int {
zeros := [0; 100]; // 100 zeros
blank := [""; 10]; // 10 empty strings
grid := [false; 64]; // 64 false values
println(zeros.len()); // 100
println(zeros[50]); // 0
return 0;
}Use repeat syntax to create empty arrays — [0; 0] gives you an empty [int] ready for .push():
fn main() -> int {
buffer := [0; 0]; // empty int array
scores := [0.0; 50]; // 50 floats, all 0.0
return 0;
}Type Annotations
Array types are written as [ElementType]. Function parameters and return types always require explicit types:
fn sum(numbers: [int]) -> int {
total := 0;
for n in numbers {
total = total + n;
}
return total;
}
fn first_or_default(items: [string], fallback: string) -> string {
if items.len() > 0 {
return items[0];
}
return fallback;
}
fn main() -> int {
vals := [10, 20, 30];
println(sum(vals)); // 60
empty: [string] = [];
println(first_or_default(empty, "none")); // none
return 0;
}Indexing
Access elements by zero-based index with brackets. Out-of-bounds access is a runtime error:
fn main() -> int {
arr := [10, 20, 30, 40, 50];
println(arr[0]); // 10
println(arr[4]); // 50
// Mutation by index
arr[2] = 99;
println(arr[2]); // 99
// Index with a variable
i := 3;
println(arr[i]); // 40
return 0;
}Warning: Accessing an index beyond the array’s length causes a runtime panic. Always check
.len()if the index is computed dynamically.
Iteration
Use for to iterate over elements. The two-variable form gives you the index (see Chapter 4):
fn main() -> int {
colors := ["red", "green", "blue"];
// Value only
for color in colors {
println(color);
}
// Index and value
for i, color in colors {
println(f"{i}: {color}");
}
// 0: red
// 1: green
// 2: blue
return 0;
}.push() and .pop()
Append to the end with .push(). Remove and return the last element with .pop():
fn main() -> int {
stack := [0; 0];
stack.push(10);
stack.push(20);
stack.push(30);
println(stack.len()); // 3
top := stack.pop();
println(top); // 30
println(stack.len()); // 2
return 0;
}A stack using push/pop:
fn main() -> int {
stack := [0; 0];
items := [5, 3, 8, 1, 9];
for item in items {
stack.push(item);
}
for stack.len() > 0 {
println(stack.pop());
}
// 9, 1, 8, 3, 5
return 0;
}.slice()
Extract a sub-array with .slice(start, end). The range is half-open — start is inclusive, end is exclusive:
fn main() -> int {
arr := [10, 20, 30, 40, 50];
first_three := arr.slice(0, 3);
println(first_three); // [10, 20, 30]
middle := arr.slice(1, 4);
println(middle); // [20, 30, 40]
last_two := arr.slice(3, 5);
println(last_two); // [40, 50]
return 0;
}.contains()
Check if an element exists in the array:
fn main() -> int {
primes := [2, 3, 5, 7, 11, 13];
println(primes.contains(7)); // true
println(primes.contains(4)); // false
allowed := ["admin", "editor", "viewer"];
role := "editor";
if allowed.contains(role) {
println("access granted");
}
return 0;
}.reverse()
Reverse an array in place:
fn main() -> int {
arr := [1, 2, 3, 4, 5];
arr.reverse();
println(arr); // [5, 4, 3, 2, 1]
return 0;
}.join()
Combine array elements into a single string with a separator:
fn main() -> int {
words := ["hello", "world"];
println(words.join(" ")); // hello world
println(words.join(", ")); // hello, world
println(words.join("")); // helloworld
numbers := [1, 2, 3];
println(numbers.join("-")); // 1-2-3
return 0;
}.filter()
Return a new array containing only elements that pass a test:
fn main() -> int {
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
evens := numbers.filter(fn(n: int) -> bool { (n % 2) == 0 });
println(evens); // [2, 4, 6, 8, 10]
big := numbers.filter(fn(n: int) -> bool { n > 5 });
println(big); // [6, 7, 8, 9, 10]
return 0;
}Filter with a named function:
fn is_positive(n: int) -> bool {
return n > 0;
}
fn main() -> int {
values := [-3, -1, 0, 2, 5, -7, 4];
positives := values.filter(is_positive);
println(positives); // [2, 5, 4]
return 0;
}.map()
Return a new array with each element transformed:
fn main() -> int {
numbers := [1, 2, 3, 4, 5];
doubled := numbers.map(fn(n: int) -> int { n * 2 });
println(doubled); // [2, 4, 6, 8, 10]
as_strings := numbers.map(fn(n: int) -> string { str(n) });
println(as_strings.join(", ")); // 1, 2, 3, 4, 5
return 0;
}.sort()
Sort an array in place using a comparator function. The comparator returns a negative integer if the first element should come before the second, positive if after, and zero if equal:
fn main() -> int {
numbers := [5, 2, 8, 1, 9, 3];
// Ascending
numbers.sort(fn(a: int, b: int) -> int { a - b });
println(numbers); // [1, 2, 3, 5, 8, 9]
// Descending
numbers.sort(fn(a: int, b: int) -> int { b - a });
println(numbers); // [9, 8, 5, 3, 2, 1]
return 0;
}Sorting strings:
fn main() -> int {
names := ["Charlie", "Alice", "Bob", "Dana"];
names.sort(fn(a: string, b: string) -> int {
if a < b { return -1; }
if a > b { return 1; }
return 0;
});
println(names.join(", ")); // Alice, Bob, Charlie, Dana
return 0;
}Chaining Array Methods
Methods like .filter() and .map() return new arrays, so you can chain them into pipelines:
fn main() -> int {
numbers := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Get the squares of even numbers
result := numbers
.filter(fn(n: int) -> bool { (n % 2) == 0 })
.map(fn(n: int) -> int { n * n });
println(result); // [4, 16, 36, 64, 100]
return 0;
}fn main() -> int {
words := ["hello", "world", "", "mog", "", "lang"];
// Remove empty strings, convert to uppercase, join
output := words
.filter(fn(s: string) -> bool { s.len() > 0 })
.map(fn(s: string) -> string { s.upper() })
.join(" ");
println(output); // HELLO WORLD MOG LANG
return 0;
}Tip: Chaining
.filter()then.map()is the most common pipeline. If you need to both filter and transform, this reads more clearly than a manual loop.
Maps
Maps are unordered key-value collections with string keys. Use them when you need to look up values by name rather than by position.
Creating Maps
Create maps with brace syntax:
fn main() -> int {
ages := { "Alice": 30, "Bob": 25, "Charlie": 35 };
config := { "host": "localhost", "port": "8080" };
return 0;
}Access and Mutation
Read values with bracket syntax. Set values the same way — writing to a key that doesn’t exist creates it:
fn main() -> int {
scores := { "math": 95, "english": 88, "science": 92 };
// Read
println(scores["math"]); // 95
// Write — update existing key
scores["math"] = 100;
println(scores["math"]); // 100
// Write — add new key
scores["history"] = 87;
println(scores["history"]); // 87
return 0;
}.has() — Checking Key Existence
Check whether a key exists before accessing it:
fn main() -> int {
m := { "name": "Alice", "city": "NYC" };
if m.has("name") {
println(m["name"]); // Alice
}
if !m.has("age") {
println("age not found");
}
return 0;
}Note: Accessing a key that doesn’t exist is a runtime error. Always use
.has()or iterate withforwhen keys are dynamic.
.len(), .keys(), .values()
fn main() -> int {
m := { "a": 1, "b": 2, "c": 3 };
println(m.len()); // 3
ks := m.keys();
println(ks); // ["a", "b", "c"] (order may vary)
vs := m.values();
println(vs); // [1, 2, 3] (order may vary)
return 0;
}.delete() — Removing Entries
fn main() -> int {
m := { "x": 10, "y": 20, "z": 30 };
m.delete("y");
println(m.len()); // 2
println(m.has("y")); // false
return 0;
}Iterating Maps
Use for key, value in map to iterate over all entries:
fn main() -> int {
prices := { "apple": 120, "banana": 80, "cherry": 300 };
for name, price in prices {
println(f"{name}: {price} cents");
}
return 0;
}Collecting keys that meet a condition:
fn main() -> int {
grades := { "Alice": 92, "Bob": 67, "Charlie": 85, "Dana": 45, "Eve": 78 };
passing := [0; 0];
for name, grade in grades {
if grade >= 70 {
passing.push(name);
}
}
println(passing.join(", "));
return 0;
}Practical Example: Word Counting
fn word_count(text: string) -> {string: int} {
counts := { "": 0 };
counts.delete(""); // start with empty map
words := text.split(" ");
for word in words {
if counts.has(word) {
counts[word] = counts[word] + 1;
} else {
counts[word] = 1;
}
}
return counts;
}
fn main() -> int {
text := "the cat sat on the mat the cat";
counts := word_count(text);
for word, n in counts {
println(f"{word}: {n}");
}
// the: 3
// cat: 2
// sat: 1
// on: 1
// mat: 1
return 0;
}Practical Example: Grouping Data
struct Student {
name: string,
grade: string,
}
fn group_by_grade(students: [Student]) -> {string: [string]} {
groups := { "": [""] };
groups.delete("");
for s in students {
if !groups.has(s.grade) {
groups[s.grade] = [0; 0];
}
groups[s.grade].push(s.name);
}
return groups;
}
fn main() -> int {
students := [
Student { name: "Alice", grade: "A" },
Student { name: "Bob", grade: "B" },
Student { name: "Charlie", grade: "A" },
Student { name: "Dana", grade: "C" },
Student { name: "Eve", grade: "B" },
];
groups := group_by_grade(students);
for grade, names in groups {
println(f"{grade}: {names.join(", ")}");
}
// A: Alice, Charlie
// B: Bob, Eve
// C: Dana
return 0;
}SoA (Struct of Arrays)
SoA flips the usual memory layout. Instead of storing an array of structs (each struct contiguous in memory), SoA stores one contiguous array per field. This improves cache performance when you iterate over a single field across many elements — common in simulations, game engines, and data processing.
When to Use SoA
Use SoA when:
- You have many instances of the same struct (hundreds or thousands)
- Your hot loops touch one or two fields at a time, not all of them
- Performance matters and you want cache-friendly access patterns
Use regular arrays of structs when:
- You have few instances
- You access all fields of each element together
- Simplicity matters more than cache behavior
Construction
Define a regular struct (see Chapter 8), then create an SoA container with soa StructName[capacity]:
struct Particle {
x: float,
y: float,
mass: float,
}
fn main() -> int {
particles := soa Particle[1000];
return 0;
}This allocates three separate arrays of 1000 elements — one for x, one for y, one for mass — instead of one array of 1000 three-field structs.
Field Access
Read and write fields using array-index-then-dot syntax:
fn main() -> int {
particles := soa Particle[100];
// Set fields
particles[0].x = 10.0;
particles[0].y = 20.0;
particles[0].mass = 5.0;
// Read fields
println(particles[0].x); // 10.0
println(particles[0].mass); // 5.0
// Initialize several elements
for i in 0..10 {
particles[i].x = i as float * 10.0;
particles[i].y = i as float * 5.0;
particles[i].mass = 1.0;
}
println(particles[5].x); // 50.0
println(particles[9].y); // 45.0
return 0;
}Tip: The syntax
particles[i].xlooks like regular struct access, but under the hood the compiler lowers it to an index into thexcolumn array. You get columnar storage with familiar syntax.
Iteration Patterns
Iterating over a single field is where SoA shines. The compiler reads from a single contiguous array, keeping the CPU cache hot:
struct Entity {
x: int,
y: int,
health: int,
speed: int,
}
fn main() -> int {
entities := soa Entity[500];
// Initialize
for i in 0..500 {
entities[i].x = i;
entities[i].y = i * 2;
entities[i].health = 100;
entities[i].speed = 5;
}
// Update all x positions — touches only the x array
for i in 0..500 {
entities[i].x = entities[i].x + entities[i].speed;
}
// Sum all health values — touches only the health array
total_health := 0;
for i in 0..500 {
total_health = total_health + entities[i].health;
}
println(total_health); // 50000
return 0;
}Practical Example: Particle Simulation
A physics step that applies gravity and updates positions. Separating the gravity pass (which only touches vy) from the position pass (which touches x, y, vx, vy) maximizes cache efficiency:
struct Particle {
x: float,
y: float,
vx: float,
vy: float,
mass: float,
}
fn step(particles: soa Particle, count: int, dt: float) {
// Apply gravity — only touches vy array
for i in 0..count {
particles[i].vy = particles[i].vy - (9.8 * dt);
}
// Update positions — touches x, y, vx, vy arrays
for i in 0..count {
particles[i].x = particles[i].x + (particles[i].vx * dt);
particles[i].y = particles[i].y + (particles[i].vy * dt);
}
}
fn main() -> int {
particles := soa Particle[1000];
for i in 0..1000 {
particles[i].x = 0.0;
particles[i].y = 100.0;
particles[i].vx = i as float * 0.1;
particles[i].vy = 0.0;
particles[i].mass = 1.0;
}
// Run 60 simulation steps
for frame in 0..60 {
step(particles, 1000, 0.016);
}
println(particles[0].y);
println(particles[500].x);
return 0;
}Practical Example: Column Operations
SoA is natural for column-oriented data processing — summing a column, finding a max, or filtering by a category all read from a single contiguous array:
struct Record {
id: int,
value: int,
category: int,
}
fn column_sum(records: soa Record, count: int) -> int {
total := 0;
for i in 0..count {
total = total + records[i].value;
}
return total;
}
fn column_max(records: soa Record, count: int) -> int {
max_val := records[0].value;
for i in 1..count {
if records[i].value > max_val {
max_val = records[i].value;
}
}
return max_val;
}
fn count_category(records: soa Record, count: int, cat: int) -> int {
n := 0;
for i in 0..count {
if records[i].category == cat {
n = n + 1;
}
}
return n;
}
fn main() -> int {
data := soa Record[100];
for i in 0..100 {
data[i].id = i;
data[i].value = (i * 7) % 50;
data[i].category = i % 3;
}
println(column_sum(data, 100));
println(column_max(data, 100));
println(count_category(data, 100, 0)); // count of category 0
return 0;
}Putting It All Together
Data Processing Pipeline
Combining arrays, maps, and structs for a complete processing workflow:
struct Sale {
product: string,
amount: int,
region: string,
}
fn total_by_region(sales: [Sale]) -> {string: int} {
totals := { "": 0 };
totals.delete("");
for sale in sales {
if totals.has(sale.region) {
totals[sale.region] = totals[sale.region] + sale.amount;
} else {
totals[sale.region] = sale.amount;
}
}
return totals;
}
fn main() -> int {
sales := [
Sale { product: "widget", amount: 100, region: "east" },
Sale { product: "gadget", amount: 250, region: "west" },
Sale { product: "widget", amount: 150, region: "west" },
Sale { product: "gizmo", amount: 75, region: "east" },
Sale { product: "gadget", amount: 200, region: "east" },
];
region_totals := total_by_region(sales);
for region, total in region_totals {
println(f"{region}: {total}");
}
// east: 375
// west: 400
return 0;
}Filter-Map-Sort Pipeline
struct Task {
title: string,
priority: int,
done: bool,
}
fn main() -> int {
tasks := [
Task { title: "Write docs", priority: 2, done: false },
Task { title: "Fix bug", priority: 1, done: false },
Task { title: "Add tests", priority: 3, done: true },
Task { title: "Review PR", priority: 1, done: false },
Task { title: "Deploy", priority: 2, done: false },
];
// Get incomplete tasks, sorted by priority
pending := tasks.filter(fn(t: Task) -> bool { !t.done });
pending.sort(fn(a: Task, b: Task) -> int { a.priority - b.priority });
for t in pending {
println(f"[P{t.priority}] {t.title}");
}
// [P1] Fix bug
// [P1] Review PR
// [P2] Write docs
// [P2] Deploy
return 0;
}Summary
| Collection | Create | Access | Iterate |
|---|---|---|---|
| Array | [1, 2, 3] or [0; n] |
arr[i] |
for item in arr |
| Map | { "key": value } |
m["key"] |
for k, v in m |
| SoA | soa Struct[n] |
soa[i].field |
for i in 0..n |
Array methods: .len(), .push(), .pop(), .slice(), .contains(), .reverse(), .filter(), .map(), .sort(), .join()
Map methods: .len(), .keys(), .values(), .has(), .delete()
Arrays are the default choice. Use maps when you need key-based lookup. Use SoA when you have many elements and need to iterate over individual fields efficiently — the syntax stays familiar while the memory layout optimizes for your access pattern.
Chapter 10: Error Handling
Mog has no exceptions. There is no throw, no invisible stack unwinding, no try/finally cleanup semantics. When a function can fail, it says so in its return type, and the caller decides what to do. Errors are values — you create them, return them, match on them, and propagate them with the same tools you use for everything else.
This design means you can always see where failures are handled by reading the code. Nothing fails silently, and nothing interrupts your control flow from a distance.
Result<T>
Result<T> is a built-in type with two variants: ok(value) wrapping a success value of type T, and err(message) wrapping an error message of type string. Any function that might fail returns a Result:
fn divide(a: int, b: int) -> Result<int> {
if b == 0 {
return err("division by zero");
}
return ok(a / b);
}You construct results directly with ok() and err() — there are no special constructors or factory functions:
fn parse_port(s: string) -> Result<int> {
n := parse_int(s)?;
if n < 0 {
return err("port cannot be negative");
}
if n > 65535 {
return err(f"port out of range: {n}");
}
return ok(n);
}Results can wrap any type, including compound types:
fn load_settings(path: string) -> Result<{string: string}> {
content := fs.read(path)?;
lines := content.split("\n");
settings: {string: string} = {};
for line in lines {
parts := line.split("=");
if parts.len() != 2 {
return err(f"malformed setting: {line}");
}
settings[parts[0].trim()] = parts[1].trim();
}
return ok(settings);
}Use match to handle both cases (see Chapter 4 for match syntax):
fn main() -> int {
match divide(100, 7) {
ok(v) => println(f"100 / 7 = {v}"),
err(e) => println(f"failed: {e}"),
}
return 0;
}You can bind the result first and match later:
fn main() -> int {
result := divide(10, 0);
match result {
ok(v) => {
println(f"result: {v}");
println(f"doubled: {v * 2}");
},
err(e) => {
println(f"division failed: {e}");
},
}
return 0;
}Match arms can contain any expression or block. A common pattern is returning from the enclosing function inside a match arm:
fn half_or_bail(n: int) -> Result<int> {
match divide(n, 2) {
ok(v) => return ok(v),
err(e) => return err(f"halving failed: {e}"),
}
}Optional ?T
?T is a built-in type with two variants: some(value) wrapping a value of type T, and none representing absence. Use it when a value might not exist — not because something went wrong, but because there may simply be nothing there:
fn find_index(arr: [int], target: int) -> ?int {
for i, v in arr {
if v == target {
return some(i);
}
}
return none;
}The ? prefix goes on the type: ?int, ?string, ?User. You construct values with some() and signal absence with none:
fn first_positive(numbers: [int]) -> ?int {
for n in numbers {
if n > 0 {
return some(n);
}
}
return none;
}struct User {
name: string,
email: string,
}
fn lookup_user(users: [User], name: string) -> ?User {
for u in users {
if u.name == name {
return some(u);
}
}
return none;
}Match on optionals the same way you match on results:
fn main() -> int {
match find_index([10, 20, 30], 20) {
some(i) => println(f"found at index {i}"),
none => println("not found"),
}
return 0;
}Tip: Optionals carry no error message. If you need to explain why a value is absent, use
Result<T>instead.
// Use ?T when absence is normal
fn get_middle_name(user: User) -> ?string {
return user.middle_name;
}
// Use Result<T> when absence is a failure
fn get_required_field(data: {string: string}, key: string) -> Result<string> {
if data.has(key) {
return ok(data[key]);
}
return err(f"missing required field: {key}");
}The ? Propagation Operator
Appending ? to a Result or ?T expression unwraps the success case and propagates the failure case. If the value is ok(v) or some(v), it evaluates to v. If the value is err(e) or none, the current function returns immediately with that error or none.
The simplest case — unwrap or bail:
fn process() -> Result<int> {
val := divide(10, 2)?; // val is 5, or function returns the err
return ok(val * 2);
}Chaining multiple fallible operations is where ? shines. Each ? is an early-return point:
fn load_and_parse_config(path: string) -> Result<Config> {
content := fs.read(path)?; // bail if read fails
json := parse_json(content)?; // bail if parse fails
config := validate_config(json)?; // bail if validation fails
return ok(config);
}Without ?, the same function requires nested matching:
fn load_and_parse_config(path: string) -> Result<Config> {
match fs.read(path) {
err(e) => return err(e),
ok(content) => {
match parse_json(content) {
err(e) => return err(e),
ok(json) => {
match validate_config(json) {
err(e) => return err(e),
ok(config) => return ok(config),
}
},
}
},
}
}The ? operator works inline in larger expressions:
fn compute_ratio(a: int, b: int, c: int) -> Result<float> {
sum := divide(a, b)? + divide(a, c)?;
return ok(sum as float);
}It also works on optionals. A function returning ?T can propagate none from another optional:
fn get_user_email(users: [User], name: string) -> ?string {
user := lookup_user(users, name)?; // returns none if not found
return some(user.email);
}Note: The return type of your function must be compatible:
?on aResultrequires your function to returnResult, and?on an optional requires your function to return an optional.
fn first_even(arr: [int]) -> ?int {
for n in arr {
if (n % 2) == 0 {
return some(n);
}
}
return none;
}
fn double_first_even(arr: [int]) -> ?int {
val := first_even(arr)?; // propagates none
return some(val * 2);
}try-catch Blocks
Sometimes you want to handle errors from a group of operations in one place rather than propagating them up. try-catch catches errors from ? propagation inside the try block:
try {
config := fs.read("config.json")?;
data := fs.read("data.csv")?;
process(config, data)?;
println("done");
} catch(e) {
println(f"setup failed: {e}");
}The variable e in catch(e) is a string — the error message from whichever ? failed. The parentheses around e are required.
try-catch is useful at the top level of a program or at the boundary of a subsystem, where you want to handle all errors uniformly:
fn run_pipeline() {
try {
input := read_input()?;
validated := validate(input)?;
result := transform(validated)?;
write_output(result)?;
println("pipeline complete");
} catch(e) {
println(f"pipeline failed: {e}");
}
}You can use try-catch inside loops:
fn process_files(paths: [string]) {
for path in paths {
try {
content := fs.read(path)?;
result := parse(content)?;
println(f"{path}: {result}");
} catch(e) {
println(f"skipping {path}: {e}");
}
}
}try-catch does not change your function’s return type. It’s a local error-handling boundary — it consumes the error instead of propagating it. If you need the function to still return Result, use ? outside the try block or return explicitly from within catch:
fn load_with_fallback(primary: string, backup: string) -> Result<string> {
try {
content := fs.read(primary)?;
return ok(content);
} catch(e) {
println(f"primary failed ({e}), trying backup");
}
// If we reach here, primary failed — try backup without catching
content := fs.read(backup)?;
return ok(content);
}Tip: A
try-catchblock lets you use?in functions that don’t returnResult. Since?would normally require aResultreturn type,try-catchgives you a way to use?in void functions.
fn initialize() {
try {
config := load_config("app.json")?;
connect_db(config.db_url)?;
println("initialized");
} catch(e) {
println(f"init failed: {e}");
process.exit(1);
}
}Match Patterns for Result and Optional
The ok, err, some, and none patterns work like any other match arms. You can combine them with guards and nested patterns.
Matching with variable binding:
fn describe_result(r: Result<int>) -> string {
match r {
ok(v) => return f"success: {v}",
err(e) => return f"failure: {e}",
}
}Matching an optional inside a struct:
struct Response {
status: int,
body: ?string,
}
fn print_response(resp: Response) {
match resp.body {
some(text) => println(f"[{resp.status}] {text}"),
none => println(f"[{resp.status}] (no body)"),
}
}Warning: Match arms for
Resultand?Tmust be exhaustive. The compiler warns if you handleokbut noterr, orsomebut notnone.
// Compiler warning: missing err arm
match divide(10, 3) {
ok(v) => println(v),
}
// Correct: handle both
match divide(10, 3) {
ok(v) => println(v),
err(e) => println(f"error: {e}"),
}Practical Patterns
Validation Chains
Build up validation by chaining multiple checks that each return Result:
fn validate_username(name: string) -> Result<string> {
if name.len() == 0 {
return err("username cannot be empty");
}
if name.len() < 3 {
return err("username must be at least 3 characters");
}
if name.len() > 32 {
return err("username must be at most 32 characters");
}
return ok(name);
}
fn validate_age(age: int) -> Result<int> {
if age < 0 {
return err("age cannot be negative");
}
if age > 150 {
return err(f"age seems unrealistic: {age}");
}
return ok(age);
}
fn create_user(name: string, age: int) -> Result<User> {
validated_name := validate_username(name)?;
validated_age := validate_age(age)?;
return ok(User { name: validated_name, age: validated_age });
}Safe Parsing
Parse user input and propagate failures naturally:
struct Point {
x: float,
y: float,
}
fn parse_point(s: string) -> Result<Point> {
parts := s.split(",");
if parts.len() != 2 {
return err(f"expected 'x,y' but got: {s}");
}
x := parse_float(parts[0].trim())?;
y := parse_float(parts[1].trim())?;
return ok(Point { x: x, y: y });
}
fn parse_polygon(lines: [string]) -> Result<[Point]> {
points: [Point] = [];
for i, line in lines {
match parse_point(line) {
ok(p) => points.push(p),
err(e) => return err(f"line {i + 1}: {e}"),
}
}
if points.len() < 3 {
return err(f"polygon needs at least 3 points, got {points.len()}");
}
return ok(points);
}Converting Between Result and Optional
Sometimes you have a Result but only care about the success case, or you have an optional but need an error message:
// Result<T> -> ?T: discard the error message
fn result_to_optional(r: Result<int>) -> ?int {
match r {
ok(v) => return some(v),
err(_) => return none,
}
}
// ?T -> Result<T>: supply an error message
fn optional_to_result(opt: ?int, msg: string) -> Result<int> {
match opt {
some(v) => return ok(v),
none => return err(msg),
}
}
fn find_or_fail(arr: [int], target: int) -> Result<int> {
match find_index(arr, target) {
some(i) => return ok(i),
none => return err(f"value {target} not found in array"),
}
}Providing Defaults for Optionals
When you have an optional and want a fallback:
fn get_port(config: {string: string}) -> int {
match config["port"] {
some(p) => {
match parse_int(p) {
ok(n) => return n,
err(_) => return 8080,
}
},
none => return 8080,
}
}Or using ? inside try-catch for a more compact version:
fn get_port(config: {string: string}) -> int {
try {
p := config["port"]?;
n := parse_int(p)?;
return n;
} catch(_) {
return 8080;
}
}Error Message Formatting
Build descriptive error messages with string interpolation (see Chapter 3):
fn load_user_record(id: int) -> Result<User> {
path := f"data/users/{id}.json";
content := match fs.read(path) {
ok(c) => c,
err(e) => return err(f"failed to read user {id}: {e}"),
};
user := match parse_user(content) {
ok(u) => u,
err(e) => return err(f"failed to parse user {id}: {e}"),
};
return ok(user);
}The same function using ? is shorter but loses the contextual error messages:
fn load_user_record(id: int) -> Result<User> {
path := f"data/users/{id}.json";
content := fs.read(path)?;
user := parse_user(content)?;
return ok(user);
}Choose based on whether callers need context. Deep library code often adds context; top-level code often just propagates.
Collecting Results
Process a list and stop at the first error, or collect all successes:
// Stop at first error
fn parse_all_ints(strings: [string]) -> Result<[int]> {
results: [int] = [];
for s in strings {
n := parse_int(s)?;
results.push(n);
}
return ok(results);
}
// Collect errors separately
fn parse_all_ints_lenient(strings: [string]) -> [int] {
results: [int] = [];
for s in strings {
match parse_int(s) {
ok(n) => results.push(n),
err(_) => {}, // skip bad values
}
}
return results;
}Nested Results
A function can return Result<Result<T>> when the outer and inner operations can both independently fail, though this is rare and usually a sign you should restructure:
fn fetch_and_parse(url: string) -> Result<Result<Config>> {
response := match http.get(url) {
ok(r) => r,
err(e) => return err(f"network error: {e}"),
};
// Return ok wrapping the parse result — caller sees network vs parse errors separately
return ok(parse_config(response.body));
}In most cases, flatten the errors into a single Result instead:
fn fetch_and_parse(url: string) -> Result<Config> {
response := http.get(url)?;
config := parse_config(response.body)?;
return ok(config);
}Summary
| Syntax | Meaning |
|---|---|
Result<T> |
A value that is either ok(T) or err(string) |
?T |
A value that is either some(T) or none |
ok(value) |
Construct a success result |
err(message) |
Construct an error result |
some(value) |
Construct a present optional |
none |
Construct an absent optional |
expr? |
Unwrap success or propagate failure |
try { ... } catch(e) { ... } |
Handle propagated errors locally |
Mog’s error handling is explicit and local. You always know which functions can fail by looking at their return type, and you always know where errors are handled by following the ? operators and match arms. There are no hidden control flow paths — what you read is what runs.
Chapter 11: Async Programming
Mog uses async/await for asynchronous operations. Agent scripts need to wait on external operations — API calls, model inference, file I/O — and async functions let you express that waiting without blocking the entire program. The host runtime manages the event loop; Mog code never creates threads or manages concurrency primitives directly.
Async Functions
Mark a function as async to indicate it returns a future. Use await inside to wait for other async operations:
async fn fetch(url: string) -> Result<string> {
response := await http.get(url)?;
return ok(response.body);
}An async fn can be called like any other function, but its return value is a future that must be awaited to get the actual result:
async fn greet(name: string) -> string {
return f"hello, {name}";
}
async fn main() -> int {
msg := await greet("world");
println(msg); // hello, world
return 0;
}Note: When
mainis declaredasync, the runtime creates the event loop automatically. You don’t need to set up or start the loop yourself.
Async functions can call other async functions. Each await suspends the current function until the awaited future completes:
async fn fetch_json(url: string) -> Result<string> {
raw := await http.get(url)?;
parsed := parse_json(raw.body)?;
return ok(parsed);
}
async fn get_user_name(id: int) -> Result<string> {
data := await fetch_json(f"https://api.example.com/users/{id}")?;
return ok(data["name"]);
}Await
The await keyword suspends execution until a future resolves. It works on any expression that produces a future:
async fn pipeline() -> Result<string> {
raw := await fetch_data("https://api.example.com/data")?;
processed := await transform(raw)?;
return ok(processed);
}Each await is a suspension point. The runtime can run other tasks while this function waits. Without await, the future is created but never resolved:
async fn example() -> Result<string> {
// This creates a future but doesn't wait for it — probably a bug
// fetch_data("https://api.example.com");
// This creates the future AND waits for the result
result := await fetch_data("https://api.example.com")?;
return ok(result);
}Warning: Forgetting
awaitis a common mistake. If you call an async function withoutawait, you get a future object, not the actual result.
You can combine await with the ? operator from Chapter 10. The await resolves the future, then ? unwraps the Result:
async fn load_config(path: string) -> Result<Config> {
content := await fs.read_async(path)?; // await the I/O, then ? the Result
config := parse_config(content)?; // synchronous parse, just ? the Result
return ok(config);
}Spawn
Use spawn to launch a task that runs in the background. The spawned task executes concurrently with the rest of your code — you don’t wait for it to finish:
async fn log_event(event: string) -> Result<int> {
await http.post("https://logs.example.com", event)?;
return ok(0);
}
async fn handle_request(req: Request) -> Result<Response> {
// Fire and forget — don't wait for logging to complete
spawn log_event(f"received request: {req.path}");
result := await process(req)?;
return ok(result);
}Spawn is useful for side effects you don’t need to wait on — logging, analytics, cache warming:
async fn warm_cache(keys: [string]) {
for key in keys {
data := await fetch_data(key)?;
cache.set(key, data);
}
}
async fn main() -> int {
// Start cache warming in the background
spawn warm_cache(["users", "products", "settings"]);
// Continue immediately without waiting
println("server starting...");
await start_server(8080);
return 0;
}Tip: Spawned tasks that fail do so silently — their errors aren’t propagated to the caller. If you need to know whether a background task succeeded, use
awaitinstead ofspawn, or useall()to collect results.
async fn main() -> int {
// Bad: error is silently lost
spawn might_fail();
// Good: error is handled
try {
await might_fail();
} catch(e) {
println(f"task failed: {e}");
}
return 0;
}all() — Wait for All
all() takes a list of futures and waits for all of them to complete. It returns when every future has resolved, giving you all the results:
async fn parallel_fetch() -> Result<[string]> {
results := await all([
fetch_data("https://api.example.com/a"),
fetch_data("https://api.example.com/b"),
fetch_data("https://api.example.com/c"),
])?;
return ok(results);
}This is significantly faster than sequential awaits when the operations are independent:
// Sequential — each waits for the previous to finish
async fn fetch_sequential() -> Result<[string]> {
a := await fetch_data("https://api.example.com/a")?;
b := await fetch_data("https://api.example.com/b")?;
c := await fetch_data("https://api.example.com/c")?;
return ok([a, b, c]);
}
// Parallel — all three run at the same time
async fn fetch_parallel() -> Result<[string]> {
results := await all([
fetch_data("https://api.example.com/a"),
fetch_data("https://api.example.com/b"),
fetch_data("https://api.example.com/c"),
])?;
return ok(results);
}Note: If any future in
all()fails, the entireall()returns that error. All futures are started concurrently, but a single failure short-circuits the result.
A practical example — loading multiple resources in parallel to build a page:
struct Page {
user: string,
posts: string,
notifications: string,
}
async fn load_page(user_id: int) -> Result<Page> {
results := await all([
fetch_data(f"https://api.example.com/users/{user_id}"),
fetch_data(f"https://api.example.com/users/{user_id}/posts"),
fetch_data(f"https://api.example.com/users/{user_id}/notifications"),
])?;
return ok(Page {
user: results[0],
posts: results[1],
notifications: results[2],
});
}race() — Wait for First
race() takes a list of futures and returns the result of whichever finishes first. The remaining futures are cancelled:
async fn fastest() -> Result<string> {
result := await race([
fetch_from_primary(),
fetch_from_backup(),
])?;
return ok(result);
}The most common use for race() is implementing timeouts:
async fn timeout(ms: int) -> Result<string> {
await sleep(ms);
return err("operation timed out");
}
async fn fetch_with_timeout(url: string) -> Result<string> {
result := await race([
fetch_data(url),
timeout(5000),
])?;
return ok(result);
}Another use — trying multiple strategies and going with the first to succeed:
async fn resolve_address(host: string) -> Result<string> {
result := await race([
dns_lookup_v4(host),
dns_lookup_v6(host),
])?;
return ok(result);
}Warning:
race()returns the first future to complete, whether it succeeds or fails. If the fastest future returns an error, that error propagates — even if a slower future would have succeeded. Design your futures accordingly.
Error Handling with Async
Async functions combine naturally with Result<T> and the ? operator from Chapter 10. The await resolves the future, and ? unwraps the result:
async fn create_user(name: string, email: string) -> Result<User> {
// Validate inputs (synchronous)
validated_name := validate_name(name)?;
validated_email := validate_email(email)?;
// Check for duplicates (async)
existing := await db.find_user_by_email(validated_email)?;
match existing {
some(_) => return err("email already registered"),
none => {},
}
// Create the user (async)
user := await db.insert_user(validated_name, validated_email)?;
return ok(user);
}Use try-catch inside async functions the same way you would in synchronous code:
async fn sync_all_data() {
sources := ["users", "products", "orders"];
for source in sources {
try {
data := await fetch_data(f"https://api.example.com/{source}")?;
await db.upsert(source, data)?;
println(f"synced {source}");
} catch(e) {
println(f"failed to sync {source}: {e}");
}
}
}A complete async program with error handling:
async fn fetch_weather(city: string) -> Result<string> {
url := f"https://weather.example.com/api?city={city}";
response := await http.get(url)?;
data := parse_json(response.body)?;
return ok(data["temperature"]);
}
async fn main() -> int {
cities := ["London", "Tokyo", "New York"];
results := await all([
fetch_weather("London"),
fetch_weather("Tokyo"),
fetch_weather("New York"),
]);
match results {
ok(temps) => {
for i, city in cities {
println(f"{city}: {temps[i]}");
}
},
err(e) => {
println(f"weather fetch failed: {e}");
},
}
return 0;
}Retry Pattern
Combine async with a loop to retry failed operations:
async fn fetch_with_retry(url: string, max_retries: int) -> Result<string> {
attempts := 0;
for attempts < max_retries {
match await http.get(url) {
ok(response) => return ok(response.body),
err(e) => {
attempts = attempts + 1;
if attempts >= max_retries {
return err(f"failed after {max_retries} attempts: {e}");
}
println(f"attempt {attempts} failed, retrying...");
await sleep(1000 * attempts); // exponential-ish backoff
},
}
}
return err("unreachable");
}Fan-out / Fan-in
Process multiple items concurrently, then aggregate the results:
async fn process_batch(urls: [string]) -> Result<[string]> {
// Build a list of futures
futures := urls.map(fn(url: string) -> Future<Result<string>> {
return fetch_data(url);
});
// Wait for all concurrently
results := await all(futures)?;
return ok(results);
}
async fn main() -> int {
urls := [
"https://api.example.com/1",
"https://api.example.com/2",
"https://api.example.com/3",
"https://api.example.com/4",
"https://api.example.com/5",
];
match await process_batch(urls) {
ok(data) => {
for i, d in data {
println(f"result {i}: {d}");
}
},
err(e) => println(f"batch failed: {e}"),
}
return 0;
}Summary
| Syntax | Meaning |
|---|---|
async fn f() -> T |
Declare an async function returning a future |
await expr |
Suspend until a future resolves |
spawn task() |
Launch a fire-and-forget background task |
all([f1, f2, f3]) |
Wait for all futures to complete |
race([f1, f2]) |
Wait for the first future to complete |
Async functions compose with Result<T> and ? from Chapter 10 — await resolves the future, then ? unwraps the result. Use all() when you have independent operations that can run in parallel. Use race() for timeouts and fallback strategies. Use spawn only for side effects you don’t need to track. The runtime manages the event loop; your job is to describe what depends on what.
Chapter 12: Modules and Packages
As Mog programs grow beyond a single file, you need a way to split code into logical units, control what’s visible to the outside, and compose libraries. Mog uses a Go-style module system: packages group related code, pub controls visibility, and import brings packages into scope.
There are no header files, no include guards, no complex build configurations. A package is a directory. A module is a project. The compiler resolves everything from the file system.
Package Declaration
Every Mog file begins with a package declaration. It names the package the file belongs to:
package math;All .mog files in the same directory must declare the same package name. The package name becomes the namespace used by importers:
// geometry/shapes.mog
package geometry;
pub fn circle_area(radius: float) -> float {
return PI * (radius ** 2.0);
}// geometry/volumes.mog
package geometry;
pub fn sphere_volume(radius: float) -> float {
return (4.0 / 3.0) * PI * (radius ** 3.0);
}Both files belong to package geometry. They share the same namespace — functions in one file can call private functions in the other, because they’re in the same package.
The package main package is special. It must contain a fn main() entry point. Every executable Mog program has exactly one package main:
package main;
fn main() -> int {
print("hello");
return 0;
}Files without a package declaration are implicitly package main. This is single-file mode — convenient for scripts and quick experiments.
Public vs Private — The pub Keyword
Symbols are package-private by default. Only symbols marked with pub are visible to importers:
package mathutil;
// Public — importers can call this
pub fn gcd(a: int, b: int) -> int {
while b != 0 {
temp := b;
b = a % b;
a = temp;
}
return a;
}
// Public — importers can call this
pub fn lcm(a: int, b: int) -> int {
return (a * b) / gcd(a, b);
}
// Private — only visible within package mathutil
fn validate_positive(n: int) -> bool {
return n > 0;
}pub works on functions, async functions, structs, and type aliases:
package models;
pub struct User {
name: string,
email: string,
age: int,
}
pub type UserList = [User];
pub fn create_user(name: string, email: string, age: int) -> User {
return User { name: name, email: email, age: age };
}
pub async fn fetch_user(id: int) -> Result<User> {
data := await http.get("http://api.example.com/users/{id}")?;
return ok(parse_user(data));
}
// Private helper — not exported
fn parse_user(data: string) -> User {
return User { name: "parsed", email: "parsed", age: 0 };
}Struct fields are always accessible when the struct itself is pub. There is no field-level visibility — if you export the struct, you export all its fields:
package config;
// Exported struct — all fields accessible to importers
pub struct Settings {
host: string,
port: int,
debug: bool,
}
// Private struct — importers cannot see this at all
struct InternalState {
cache: [string],
dirty: bool,
}Tip: If you need to hide a struct’s internals, keep the struct private and export functions that construct and inspect it. This is the idiomatic way to build opaque types in Mog.
Importing Packages
Use import to bring a package into scope. Access its public symbols with qualified names — package.symbol:
package main;
import mathutil;
fn main() -> int {
result := mathutil.gcd(48, 18);
print("GCD: {result}"); // GCD: 6
return 0;
}The import name matches the package name. After importing, every public function, struct, and type from that package is available through the qualified prefix:
package main;
import models;
fn main() -> int {
user := models.create_user("alice", "alice@example.com", 30);
print(user.name); // alice
print(user.email); // alice@example.com
return 0;
}Importing a package does not import its symbols into the local namespace. You always use the qualified form. This keeps names unambiguous when multiple packages are in play:
package main;
import math;
import physics;
fn main() -> int {
// Clear which 'distance' function you mean
d1 := math.distance(0.0, 0.0, 3.0, 4.0);
d2 := physics.distance(10.0, 9.8, 2.0);
print("math: {d1}, physics: {d2}");
return 0;
}Multi-Import Syntax
When importing several packages, use the grouped form:
package main;
import (math, utils, models, config);
fn main() -> int {
settings := config.load_defaults();
data := utils.read_input("data.txt");
result := math.dot_product(data, data);
print(result);
return 0;
}This is equivalent to writing four separate import statements. The grouped form is preferred when a file has three or more imports:
package main;
// Equivalent — but the grouped form is cleaner
import math;
import utils;
import models;
import config;Qualified Access
All access to imported symbols uses the package.name form. This applies to functions, structs, and type aliases:
package main;
import geometry;
fn main() -> int {
// Function call
area := geometry.circle_area(5.0);
// Struct construction
p := geometry.Point { x: 10.0, y: 20.0 };
// Struct field access
print("area: {area}, x: {p.x}");
return 0;
}Qualified access also works in type annotations:
package main;
import models;
fn process_users(users: [models.User]) -> [string] {
names: [string] = [];
for user in users {
names.push(user.name);
}
return names;
}
fn main() -> int {
users := [
models.create_user("alice", "a@x.com", 30),
models.create_user("bob", "b@x.com", 25),
];
names := process_users(users);
for name in names {
print(name);
}
return 0;
}The Module File
Every Mog project has a mog.mod file at its root. It declares the module path — the name that identifies the entire project:
module myappThat’s it. No version numbers, no dependency lists — just the module name. The module file tells the compiler where the project root is, and the directory structure below it defines the packages.
A typical project layout:
myapp/
mog.mod // module myapp
main.mog // package main — entry point
math/
math.mog // package math
trig.mog // package math (same package, same dir)
models/
user.mog // package models
post.mog // package models
utils/
strings.mog // package utilsThe compiler resolves import math by looking for a math/ directory relative to the module root. Every .mog file in that directory is part of the math package.
Note: The directory name and package name must match. A file in
math/that declarespackage geometryis a compile error.
Circular Import Detection
Mog does not allow circular imports. If package A imports package B and package B imports package A, the compiler rejects the program with an error:
// a/a.mog
package a;
import b; // a depends on b
pub fn from_a() -> int {
return b.from_b() + 1;
}// b/b.mog
package b;
import a; // b depends on a — COMPILE ERROR: circular import
pub fn from_b() -> int {
return a.from_a() + 1;
}The fix is to extract shared code into a third package that both A and B can import:
// shared/shared.mog
package shared;
pub fn base_value() -> int {
return 42;
}// a/a.mog
package a;
import shared;
pub fn from_a() -> int {
return shared.base_value() + 1;
}// b/b.mog
package b;
import shared;
pub fn from_b() -> int {
return shared.base_value() + 2;
}Warning: Circular dependencies are always a compile error — there is no way to forward-declare or defer imports. If you hit this, it usually means two packages are too tightly coupled and should share a common dependency.
Practical Example: Splitting a Program into Packages
Here is a small project that fetches user data, processes it, and writes output — split across three packages.
Project structure:
userapp/
mog.mod
main.mog
api/
api.mog
transform/
transform.mogmog.mod:
module userappapi/api.mog — handles network communication:
package api;
requires http;
pub struct UserData {
name: string,
score: int,
}
pub async fn fetch_user(id: int) -> Result<UserData> {
body := await http.get("http://api.example.com/users/{id}")?;
return ok(parse(body));
}
pub async fn fetch_users(ids: [int]) -> Result<[UserData]> {
futures := ids.map(fn(id) { fetch_user(id) });
results := await all(futures)?;
return ok(results);
}
fn parse(body: string) -> UserData {
return UserData { name: body, score: 0 };
}transform/transform.mog — pure data transformations:
package transform;
import api;
pub fn top_scorers(users: [api.UserData], threshold: int) -> [api.UserData] {
result: [api.UserData] = [];
for user in users {
if user.score >= threshold {
result.push(user);
}
}
return result;
}
pub fn format_report(users: [api.UserData]) -> string {
report := "User Report\n";
report = report + "===========\n";
for i, user in users {
report = report + "{i + 1}. {user.name} (score: {user.score})\n";
}
return report;
}main.mog — entry point that wires everything together:
package main;
import (api, transform);
requires fs;
async fn main() -> int {
ids := [1, 2, 3, 4, 5];
match await api.fetch_users(ids) {
ok(users) => {
top := transform.top_scorers(users, 80);
report := transform.format_report(top);
print(report);
await fs.write_file("report.txt", report)?;
},
err(msg) => print("failed to fetch users: {msg}"),
}
return 0;
}Each package has a single responsibility. The api package handles network calls and data parsing. The transform package contains pure functions that process data. The main package wires them together. Private functions like parse in the api package stay hidden — importers see only what’s marked pub.
Summary
Mog’s module system keeps things flat and explicit. There are no nested modules, no re-exports, no renaming on import. A package is a directory, pub controls visibility, and qualified access eliminates naming conflicts.
Key points:
- Declare packages with
package name;at the top of every file - Export symbols with
pub— everything else is package-private - Import with
import name;orimport (a, b, c);for groups - Access imported symbols with
package.name— always qualified, never bare - Structure projects with one directory per package and
mog.modat the root - Avoid circular imports — extract shared code into a common package
Capabilities (Chapter 13) use the same dot-syntax as package access — fs.read_file() looks like a module call but is backed by host-provided functions rather than Mog source code. The next chapter explains how that works.
Chapter 13: Capabilities — Safe I/O
Mog has no built-in I/O. No file reads, no network calls, no environment variables — nothing that touches the outside world lives in the language itself. All side effects flow through capabilities: named interfaces that the host application provides to the script at runtime.
This is the foundation of Mog’s security model. If the host doesn’t grant a capability, the script can’t use it. There’s no escape hatch, no FFI backdoor, no unsafe block. A Mog script can only do what the host explicitly allows.
The Capability Model
A capability is a named collection of functions provided by the host. From the script’s perspective, it looks like a module with dot-syntax method calls:
requires fs;
async fn main() -> int {
content := await fs.read_file("config.json")?;
print(content);
return 0;
}The key difference from a regular module: there is no Mog source code behind fs. The host application implements those functions in C (or whatever language hosts the VM) and registers them before the script runs.
This design has three consequences:
- Sandboxing is the default. A script with no
requiresdeclaration has zero access to the outside world. It can compute, but it can’t affect anything. - The compiler enforces declarations. If you call
fs.read_filewithout declaringrequires fs, the compiler rejects your program. - The host enforces availability. If a script declares
requires httpbut the host doesn’t providehttp, the host rejects the script before it runs.
Declaring Capabilities: requires and optional
Capability declarations go at the top of the file, before any function definitions:
requires fs, process;
optional log;requires means the program cannot run without these capabilities. If any are missing, the host refuses to execute the script:
requires fs, process;
async fn main() -> int {
content := await fs.read_file("data.txt")?;
dir := process.cwd();
print("read from {dir}");
return 0;
}optional means the program can function without these capabilities but will use them if available. You check at runtime whether an optional capability is present:
requires fs;
optional log;
async fn main() -> int {
data := await fs.read_file("input.txt")?;
// log may or may not be available
log.info("loaded input file");
return 0;
}A program with no declarations at all is a pure computation — it takes no input and produces no output beyond its return value:
fn fibonacci(n: int) -> int {
if n <= 1 { return n; }
a := 0;
b := 1;
for i in 2..(n + 1) {
temp := a + b;
a = b;
b = temp;
}
return b;
}
fn main() -> int {
return fibonacci(10); // 55
}Note: Capabilities use the same dot-syntax as package imports (Chapter 12), but they are backed by host-provided C functions, not Mog source code. The compiler knows the difference because capabilities are declared with
requires/optional, notimport.
Built-in Capabilities
The Mog runtime ships with a POSIX host that provides two standard capabilities: fs and process. These are conventional names — the host registers them, not the language.
fs — File System
The fs capability provides file operations. Some may be async under the hood depending on the host, but the Mog interface uses await:
requires fs;
async fn main() -> int {
// Read an entire file as a string
content := await fs.read_file("data.txt")?;
print(content);
// Write a string to a file (creates or overwrites)
await fs.write_file("output.txt", "hello, world")?;
// Append to a file
await fs.append_file("log.txt", "new entry\n")?;
// Check if a file exists
if await fs.exists("config.json")? {
print("config found");
}
// Get file size in bytes
size := await fs.file_size("data.txt")?;
print("file is {size} bytes");
// Remove a file
await fs.remove("temp.txt")?;
return 0;
}The full fs interface:
| Function | Signature | Description |
|---|---|---|
read_file |
(path: string) -> string |
Read entire file contents |
write_file |
(path: string, contents: string) -> int |
Write string to file |
append_file |
(path: string, contents: string) -> int |
Append string to file |
exists |
(path: string) -> bool |
Check if path exists |
remove |
(path: string) -> int |
Delete a file |
file_size |
(path: string) -> int |
Get file size in bytes |
process — Process and Environment
The process capability provides access to the runtime environment:
requires process;
async fn main() -> int {
// Sleep for 500 milliseconds
await process.sleep(500);
// Get current timestamp (milliseconds since Unix epoch)
now := process.timestamp();
print("current time: {now}");
// Get the current working directory
dir := process.cwd();
print("working in: {dir}");
// Read an environment variable
home := process.getenv("HOME");
print("home directory: {home}");
// Exit with a specific code
process.exit(0);
return 0;
}The full process interface:
| Function | Signature | Description |
|---|---|---|
sleep |
async (ms: int) -> int |
Pause execution for ms milliseconds |
timestamp |
() -> int |
Milliseconds since Unix epoch |
cwd |
() -> string |
Current working directory |
getenv |
(name: string) -> string |
Read environment variable |
exit |
(code: int) -> int |
Terminate the program |
Practical Examples
Copying a File
requires fs;
async fn copy_file(src: string, dst: string) -> int {
content := await fs.read_file(src)?;
await fs.write_file(dst, content)?;
return 0;
}
async fn main() -> int {
await copy_file("original.txt", "backup.txt")?;
print("file copied");
return 0;
}Reading Configuration
requires fs, process;
async fn main() -> int {
// Try multiple config locations
home := process.getenv("HOME");
paths := [
".config.json",
"{home}/.mogrc",
"/etc/mog/config.json",
];
for path in paths {
if await fs.exists(path)? {
config := await fs.read_file(path)?;
print("loaded config from {path}");
return 0;
}
}
print("no config file found");
return 1;
}Timed Operations
requires process;
async fn main() -> int {
start := process.timestamp();
// Do some work
sum := 0;
for i in 0..1000000 {
sum = sum + i;
}
elapsed := process.timestamp() - start;
print("computed sum={sum} in {elapsed}ms");
return 0;
}Simple Logger Using fs
requires fs, process;
async fn log(message: string) -> int {
ts := process.timestamp();
line := "[{ts}] {message}\n";
await fs.append_file("app.log", line)?;
return 0;
}
async fn main() -> int {
await log("application started")?;
await log("processing data")?;
result := do_work();
await log("finished with result: {result}")?;
return 0;
}
fn do_work() -> int {
// pure computation — no capabilities needed
total := 0;
for i in 1..101 {
total = total + (i * i);
}
return total;
}Tip: Keep capability-using functions separate from pure computation. This makes your code easier to test — pure functions need no host at all. Notice how
do_workabove has norequiresand noawait.
Conventional Capabilities
Beyond fs and process, hosts commonly provide these capabilities. None are built into the language — they are conventions that keep Mog scripts portable across different host applications:
| Capability | What it provides |
|---|---|
http |
HTTP requests |
log |
Structured logging (info, warn, error, debug) |
env |
Environment info, timestamps, random numbers |
ml |
ML operations (matmul, activations, autograd) |
model |
LLM inference |
db |
Database queries |
A host can also define entirely custom capabilities — there is nothing special about the ones listed above.
Custom Host Capabilities
Any host application can define its own capabilities with whatever functions make sense for the domain.
.mogdecl Files
Custom capabilities are declared in .mogdecl files. These files tell the compiler what functions a capability provides, so it can type-check calls at compile time:
capability env {
fn get_name() -> string
fn get_version() -> int
fn timestamp() -> int
fn random(min: int, max: int) -> int
fn log(message: string)
async fn delay_square(value: int, delay_ms: int) -> int
}A .mogdecl file contains no implementation — it’s a type declaration. The host provides the actual function bodies in C (or whatever the host language is).
Here’s the declaration for the built-in fs capability:
capability fs {
fn read_file(path: string) -> string
fn write_file(path: string, contents: string) -> int
fn append_file(path: string, contents: string) -> int
fn exists(path: string) -> bool
fn remove(path: string) -> int
fn file_size(path: string) -> int
}And process:
capability process {
async fn sleep(ms: int) -> int
fn getenv(name: string) -> string
fn cwd() -> string
fn exit(code: int) -> int
fn timestamp() -> int
}Using a Custom Capability
Once the host registers a capability and provides a matching .mogdecl file, a Mog script uses it exactly like a built-in:
requires env;
async fn main() -> int {
name := env.get_name();
version := env.get_version();
print("running {name} v{version}");
// Call an async host function
result := await env.delay_square(7, 100)?;
print("7 squared (after 100ms delay): {result}");
// Get a random number from the host
roll := env.random(1, 6);
print("dice roll: {roll}");
return 0;
}How the Pieces Fit Together
The flow from script to host and back:
- The host application starts and creates a
MogVM. - The host registers capability implementations — C functions grouped by capability name.
- The compiler reads
.mogdeclfiles and validates that every capability call in the script matches a declared function signature. - At runtime, when the script calls
env.random(1, 6), the VM routes the call throughmog_cap_call_out()to the host’s C function. - The host function receives the arguments as
MogValues, does its work, and returns aMogValueresult.
The script never knows or cares how the host implements the functions. The .mogdecl file is the contract between the two sides.
Note: For details on implementing host functions in C and registering capabilities with the VM, see Chapter 14: Embedding Mog.
Capability Validation Example
requires fs, process, http;
async fn main() -> int {
data := await http.get("https://api.example.com/data")?;
await fs.write_file("cached.json", data)?;
return 0;
}If the host only provides fs and process but not http, the script is rejected before execution. The host calls mog_validate_capabilities() and gets back an error indicating that http is missing. This is a deliberate, early failure — no partial execution, no runtime surprise.
Summary
| Concept | Meaning |
|---|---|
requires fs, process; |
Script needs these capabilities to run |
optional log; |
Script can use these if available |
.mogdecl file |
Type declaration for a custom capability |
fs.read_file(path) |
Call a capability function |
| No declaration | Pure computation, no I/O |
Capabilities are the only way for Mog code to interact with the outside world. This constraint is what makes Mog safe to embed — the host is always in control. The next chapter shows how to set up that host: creating a VM, registering capabilities from C, and enforcing resource limits.
Chapter 14: Embedding Mog in a Host Application
Mog is designed to be embedded. It’s a scripting language that runs inside your application, not a standalone runtime. The host creates a VM, decides what the script can do, enforces resource limits, and tears everything down when it’s finished.
This chapter covers the embedding API. Rust is now the primary host language, though C hosts continue to work.
The Embedding Lifecycle
Every embedded Mog program follows the same five-step lifecycle:
[Host Application]
1. Create a MogVM
2. Register capabilities (what the script can do)
3. Set resource limits (how long, how much memory)
4. Compile and run the Mog script
5. Free the VMHere’s the minimal version in Rust (compiled with mogc --link host.rs):
// host.rs — Rust host for a Mog program
use std::ptr;
extern "C" {
fn mog_vm_new() -> *mut u8;
fn mog_vm_set_global(vm: *mut u8);
fn mog_register_posix_host(vm: *mut u8);
fn mog_vm_set_limits(vm: *mut u8, limits: *const MogLimits);
fn mog_vm_free(vm: *mut u8);
fn program_user() -> i32;
}
#[repr(C)]
struct MogLimits {
max_memory: u64,
max_cpu_ms: u64,
max_stack_depth: u64,
initial_memory: u64,
}
// Pre-main initialization on macOS — runs before main()
#[unsafe(link_section = "__DATA,__mod_init_func")]
#[used]
static INIT: unsafe extern "C" fn() = {
unsafe extern "C" fn init() {
let vm = mog_vm_new();
mog_vm_set_global(vm);
mog_register_posix_host(vm);
// initial_memory is the starting GC threshold (0 = default start at 1MB).
let limits = MogLimits {
max_memory: 0,
max_cpu_ms: 5000,
max_stack_depth: 0,
initial_memory: 0,
};
mog_vm_set_limits(vm, &limits);
}
init
};The --link flag tells mogc to compile and link host files alongside the generated code. mog_vm_set_global() stores the VM pointer in a global so that generated code can find it. program_user() is the entry point the Mog compiler produces from the script’s main function.
The same lifecycle works from C — use --link host.c instead:
#include "mog.h"
int main(void) {
MogVM *vm = mog_vm_new();
mog_vm_set_global(vm);
mog_register_posix_host(vm);
MogLimits limits = { .max_cpu_ms = 5000 };
mog_vm_set_limits(vm, &limits);
int result = program_user();
mog_vm_free(vm);
return result;
}The Embedding API
The API is defined as C-compatible functions, callable from both Rust (extern "C") and C (#include "mog.h"). The --link file.rs flag compiles Rust host files and links them with the generated code. --link file.c does the same for C files.
VM Lifecycle
// Create a new VM instance
MogVM *vm = mog_vm_new();
// Store as the global VM (required for generated code)
mog_vm_set_global(vm);
// Retrieve the global VM from anywhere
MogVM *vm = mog_vm_get_global();
// Destroy the VM and free all resources
mog_vm_free(vm);A program typically creates one VM and sets it as the global. The global pointer is what the compiler-generated code uses to route capability calls back to the host.
Registering Capabilities
Capabilities are registered as arrays of name-function pairs, terminated by a {NULL, NULL} sentinel:
// Define host functions
static MogValue my_get_name(MogVM *vm, MogArgs *args) {
(void)vm; (void)args;
return mog_string("MyApp");
}
static MogValue my_get_version(MogVM *vm, MogArgs *args) {
(void)vm; (void)args;
return mog_int(42);
}
// Build the registration table
static const MogCapEntry app_functions[] = {
{ "get_name", my_get_name },
{ "get_version", my_get_version },
{ NULL, NULL } // sentinel
};
// Register under the capability name "app"
mog_register_capability(vm, "app", app_functions);After this, any Mog script that declares requires app; can call app.get_name() and app.get_version().
For the standard POSIX capabilities (fs and process), there’s a convenience function:
// Registers both "fs" and "process" capabilities
mog_register_posix_host(vm);Validating Script Requirements
Before running a script, you can check that the VM provides everything the script needs:
const char *required[] = { "fs", "process", "http", NULL };
int result = mog_validate_capabilities(vm, required);
if (result != 0) {
// Some required capability is missing — refuse to run
fprintf(stderr, "script requires capabilities this host doesn't provide\n");
return 1;
}You can also check individual capabilities:
if (mog_has_capability(vm, "http")) {
// http is available
}Note: Capability validation is covered from the Mog side in Chapter 13. The
requiresdeclaration is the script’s half of the contract;mog_validate_capabilities()is the host’s half.
Implementing a Custom Capability
Here’s a complete example: implementing an env capability that provides application metadata, random numbers, logging, and an async delay function.
Step 1: Write the .mogdecl File
capability env {
fn get_name() -> string
fn get_version() -> int
fn timestamp() -> int
fn random(min: int, max: int) -> int
fn log(message: string)
async fn delay_square(value: int, delay_ms: int) -> int
}This tells the compiler what functions exist and what their types are. See Chapter 13 for more on .mogdecl files.
Step 2: Implement the Host Functions
Every host function has the same C-compatible signature: it takes a MogVM* and MogArgs* and returns a MogValue. Here is the Rust implementation (compiled via mogc --link host.rs):
// host.rs — Rust host implementing the "env" capability
use std::ffi::{CStr, CString};
use std::time::{SystemTime, UNIX_EPOCH};
#[repr(C)]
struct MogValue { tag: i32, data: [u8; 16] }
#[repr(C)]
struct MogCapEntry { name: *const i8, func: Option<unsafe extern "C" fn(*mut u8, *mut u8) -> MogValue> }
extern "C" {
fn mog_vm_new() -> *mut u8;
fn mog_vm_set_global(vm: *mut u8);
fn mog_register_capability(vm: *mut u8, name: *const i8, entries: *const MogCapEntry);
fn mog_string(s: *const i8) -> MogValue;
fn mog_int(v: i64) -> MogValue;
fn mog_none() -> MogValue;
fn mog_arg_int(args: *mut u8, index: i32) -> i64;
fn mog_arg_string(args: *mut u8, index: i32) -> *const i8;
}
unsafe extern "C" fn host_get_name(_vm: *mut u8, _args: *mut u8) -> MogValue {
mog_string(c"MogShowcase".as_ptr())
}
unsafe extern "C" fn host_get_version(_vm: *mut u8, _args: *mut u8) -> MogValue {
mog_int(1)
}
unsafe extern "C" fn host_timestamp(_vm: *mut u8, _args: *mut u8) -> MogValue {
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
mog_int(ts)
}
unsafe extern "C" fn host_log(_vm: *mut u8, args: *mut u8) -> MogValue {
let msg = CStr::from_ptr(mog_arg_string(args, 0));
eprintln!("[mog] {}", msg.to_str().unwrap_or("?"));
mog_none()
}
#[no_mangle]
static ENV_FUNCTIONS: [MogCapEntry; 5] = [
MogCapEntry { name: c"get_name".as_ptr(), func: Some(host_get_name) },
MogCapEntry { name: c"get_version".as_ptr(), func: Some(host_get_version) },
MogCapEntry { name: c"timestamp".as_ptr(), func: Some(host_timestamp) },
MogCapEntry { name: c"log".as_ptr(), func: Some(host_log) },
MogCapEntry { name: std::ptr::null(), func: None }, // sentinel
];
#[unsafe(link_section = "__DATA,__mod_init_func")]
#[used]
static INIT: unsafe extern "C" fn() = {
unsafe extern "C" fn init() {
let vm = mog_vm_new();
mog_register_capability(vm, c"env".as_ptr(), ENV_FUNCTIONS.as_ptr());
mog_vm_set_global(vm);
}
init
};The same capability can be implemented in C (compiled via mogc --link host.c):
#include "mog.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static MogValue host_get_name(MogVM *vm, MogArgs *args) {
(void)vm; (void)args;
return mog_string("MogShowcase");
}
static MogValue host_get_version(MogVM *vm, MogArgs *args) {
(void)vm; (void)args;
return mog_int(1);
}
static MogValue host_timestamp(MogVM *vm, MogArgs *args) {
(void)vm; (void)args;
return mog_int((int64_t)time(NULL));
}
static MogValue host_random(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t min_val = mog_arg_int(args, 0);
int64_t max_val = mog_arg_int(args, 1);
if (max_val <= min_val) return mog_int(min_val);
int64_t range = max_val - min_val + 1;
return mog_int(min_val + (rand() % range));
}
static MogValue host_log(MogVM *vm, MogArgs *args) {
(void)vm;
const char *message = mog_arg_string(args, 0);
printf("[mog] %s\n", message);
return mog_none();
}Step 3: Build the Registration Table and Register
In Rust, the registration table is a static array of MogCapEntry structs (shown in Step 2 above). The #[unsafe(link_section = "__DATA,__mod_init_func")] attribute ensures registration runs before main() on macOS.
The equivalent C registration:
static const MogCapEntry env_functions[] = {
{ "get_name", host_get_name },
{ "get_version", host_get_version },
{ "timestamp", host_timestamp },
{ "random", host_random },
{ "log", host_log },
{ NULL, NULL }
};
// In your setup code:
MogVM *vm = mog_vm_new();
mog_register_capability(vm, "env", env_functions);
mog_vm_set_global(vm);Step 4: Use It from Mog
requires env;
fn main() -> int {
name := env.get_name();
version := env.get_version();
env.log("starting {name} v{version}");
roll := env.random(1, 100);
env.log("random roll: {roll}");
ts := env.timestamp();
env.log("timestamp: {ts}");
return 0;
}The compiler checks the calls against env.mogdecl. At runtime, the VM routes each call to the corresponding host function (whether implemented in Rust or C).
MogValue: Data Exchange Between Host and Script
All data crossing the host-script boundary is wrapped in MogValue, a 24-byte tagged union:
typedef struct {
enum {
MOG_INT, // int64_t
MOG_FLOAT, // double
MOG_BOOL, // bool
MOG_STRING, // const char*
MOG_NONE, // no value
MOG_HANDLE, // opaque pointer + type name
MOG_ERROR // error message string
} tag;
union {
int64_t i;
double f;
bool b;
const char *s;
struct { void *ptr; const char *type_name; } handle;
const char *error;
} data;
} MogValue;Constructing Values
Return values to the script using constructor functions:
MogValue v1 = mog_int(42);
MogValue v2 = mog_float(3.14);
MogValue v3 = mog_bool(true);
MogValue v4 = mog_string("hello");
MogValue v5 = mog_none();
MogValue v6 = mog_error("file not found");
MogValue v7 = mog_handle(my_pointer, "DatabaseConn");There are also result helpers that read more naturally for success returns:
return mog_ok_int(42); // same as mog_int(42)
return mog_ok_float(3.14); // same as mog_float(3.14)
return mog_ok_string("ok"); // same as mog_string("ok")Extracting Arguments
Read arguments from MogArgs with bounds-checked extractors. These abort on type mismatch — a host programming error, not a script error:
static MogValue host_add(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t a = mog_arg_int(args, 0); // first argument as int
int64_t b = mog_arg_int(args, 1); // second argument as int
return mog_int(a + b);
}
static MogValue host_greet(MogVM *vm, MogArgs *args) {
(void)vm;
const char *name = mog_arg_string(args, 0);
printf("hello, %s\n", name);
return mog_none();
}
static MogValue host_scale(MogVM *vm, MogArgs *args) {
(void)vm;
double value = mog_arg_float(args, 0);
double factor = mog_arg_float(args, 1);
return mog_float(value * factor);
}You can also extract from standalone MogValues (not wrapped in MogArgs):
MogValue result = mog_cap_call(vm, "math", "add", args, 2);
int64_t sum = mog_as_int(result);Returning Errors
Host functions signal errors by returning a MogValue with the MOG_ERROR tag. The script sees this as an error value that propagates through ? (see Chapter 10):
static MogValue host_read_sensor(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t sensor_id = mog_arg_int(args, 0);
if (sensor_id < 0 || sensor_id > 7) {
return mog_error("invalid sensor id");
}
double reading = read_hardware_sensor(sensor_id);
return mog_float(reading);
}From the Mog side:
requires hw;
async fn main() -> int {
// The ? propagates the error if sensor_id is invalid
temp := hw.read_sensor(3)?;
print("sensor 3: {temp}");
// This will produce an error
bad := hw.read_sensor(99)?; // error: "invalid sensor id"
return 0;
}Opaque Handles
For host-side resources that shouldn’t be inspected by the script (database connections, file handles, GPU contexts), use MOG_HANDLE:
static MogValue host_db_connect(MogVM *vm, MogArgs *args) {
(void)vm;
const char *connstr = mog_arg_string(args, 0);
DBConn *conn = db_open(connstr);
if (!conn) return mog_error("connection failed");
return mog_handle(conn, "DBConn");
}
static MogValue host_db_query(MogVM *vm, MogArgs *args) {
(void)vm;
// Extract handle with type checking — aborts if type doesn't match
DBConn *conn = mog_arg_handle(args, 0, "DBConn");
const char *sql = mog_arg_string(args, 1);
// ... execute query ...
return mog_string(result_json);
}Warning: Opaque handles are not garbage-collected. The host is responsible for managing the lifetime of the underlying resource. If the script drops a handle without calling a cleanup function, the host must detect this (e.g., via a finalizer or VM teardown hook) to avoid leaks.
Resource Limits and Timeouts
An embedded script should never be able to freeze or crash the host. Mog provides three mechanisms for resource control.
Memory Limits
Set a maximum heap size and an initial GC threshold:
MogLimits limits = {
.max_memory = 64 * 1024 * 1024, // 64MB hard cap
.max_cpu_ms = 0,
.max_stack_depth = 0,
.initial_memory = 4 * 1024 * 1024, // collect after ~4MB growth
};
mog_vm_set_limits(vm, &limits);If allocations approach the configured max_memory, the collector runs eagerly to
recover memory before rejecting further growth. If collection cannot free enough,
an interrupt is requested so the host can regain control.
CPU Time Limits
Set a maximum execution time via MogLimits:
MogLimits limits = {
.max_memory = 0, // 0 = unlimited
.max_cpu_ms = 5000, // 5 seconds
.max_stack_depth = 0, // 0 = default (1024)
.initial_memory = 0, // 0 = runtime default start threshold
};
mog_vm_set_limits(vm, &limits);When max_cpu_ms is set, the VM automatically arms a timeout. If the script exceeds the time limit, it is interrupted at the next loop back-edge and returns MOG_INTERRUPT_CODE (-99) to the host.
Manual Timeout Arming
For finer control, arm and cancel timeouts directly:
// Arm: interrupt after 3 seconds
mog_arm_timeout(3000);
int result = program_user();
if (result == MOG_INTERRUPT_CODE) {
printf("script timed out\n");
}
// Cancel if script finished early
mog_cancel_timeout();Host-Initiated Interrupts
The interrupt mechanism is thread-safe. A watchdog thread, signal handler, or UI callback can request termination at any time:
// From any thread:
mog_request_interrupt();The script checks the interrupt flag at every loop back-edge. When it sees the flag, it stops execution and returns MOG_INTERRUPT_CODE to the host. This is cooperative — not a signal kill — so the VM stays in a clean state.
// Clear the flag before running another script
mog_clear_interrupt();
// Check if an interrupt was requested
if (mog_interrupt_requested()) {
printf("interrupt pending\n");
}Tip: The cooperative interrupt model means a script that blocks inside a host function (e.g., a long-running Rust or C call) cannot be interrupted until it returns to Mog code. Keep host functions short, or implement your own cancellation within long-running host functions.
Complete Timeout Example
#include "mog.h"
#include <stdio.h>
int main(void) {
MogVM *vm = mog_vm_new();
mog_vm_set_global(vm);
mog_register_posix_host(vm);
// 2-second timeout
MogLimits limits = { .max_cpu_ms = 2000 };
mog_vm_set_limits(vm, &limits);
// Run the script
int result = program_user();
if (result == MOG_INTERRUPT_CODE) {
fprintf(stderr, "script exceeded 2-second time limit\n");
} else {
printf("script exited with code %d\n", result);
}
mog_vm_free(vm);
return (result == MOG_INTERRUPT_CODE) ? 1 : result;
}Safety Guarantees
Mog’s embedding model provides several layers of safety:
No raw pointers. Mog scripts cannot construct or dereference pointers. The only way to hold a host resource is through an opaque MOG_HANDLE, and the host controls what operations are valid on it.
No system calls without capabilities. There is no syscall(), no exec(), no way to touch the OS except through capability functions the host explicitly registered. Chapter 13 explains the capability model in detail.
GC-managed memory. The script cannot leak memory or cause use-after-free. The VM’s garbage collector handles all allocations.
Cooperative interrupts. The compiler inserts interrupt checks at every loop back-edge. An infinite loop can always be stopped by the host — no need for SIGKILL or process termination.
Capability validation. Before execution, mog_validate_capabilities() confirms that every capability the script requires is registered. Missing capabilities are caught before a single instruction runs.
Practical Example: Embedding in a Game Server
Consider a game server that lets players write Mog scripts to customize NPC behavior. The host exposes a game capability:
The .mogdecl File
capability game {
fn get_npc_health(npc_id: int) -> int
fn get_npc_position(npc_id: int) -> string
fn move_npc(npc_id: int, x: int, y: int)
fn npc_say(npc_id: int, message: string)
fn get_nearest_player(npc_id: int) -> int
fn distance_to(npc_id: int, target_id: int) -> int
}The Host Implementation ©
static MogValue game_get_npc_health(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t npc_id = mog_arg_int(args, 0);
NPC *npc = find_npc(npc_id);
if (!npc) return mog_error("unknown npc");
return mog_int(npc->health);
}
static MogValue game_move_npc(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t npc_id = mog_arg_int(args, 0);
int64_t x = mog_arg_int(args, 1);
int64_t y = mog_arg_int(args, 2);
NPC *npc = find_npc(npc_id);
if (!npc) return mog_error("unknown npc");
npc->x = (int)x;
npc->y = (int)y;
return mog_none();
}
static MogValue game_npc_say(MogVM *vm, MogArgs *args) {
(void)vm;
int64_t npc_id = mog_arg_int(args, 0);
const char *msg = mog_arg_string(args, 1);
broadcast_chat(npc_id, msg);
return mog_none();
}
// ... remaining functions ...
static const MogCapEntry game_functions[] = {
{ "get_npc_health", game_get_npc_health },
{ "get_npc_position", game_get_npc_position },
{ "move_npc", game_move_npc },
{ "npc_say", game_npc_say },
{ "get_nearest_player",game_get_nearest_player},
{ "distance_to", game_distance_to },
{ NULL, NULL }
};The Player Script (Mog)
requires game;
fn tick(npc_id: int) -> int {
health := game.get_npc_health(npc_id);
// If low health, flee
if health < 20 {
game.npc_say(npc_id, "I must retreat!");
game.move_npc(npc_id, 0, 0); // move to safe zone
return 0;
}
// Otherwise, approach the nearest player
player := game.get_nearest_player(npc_id);
dist := game.distance_to(npc_id, player);
if dist < 5 {
game.npc_say(npc_id, "Welcome, traveler!");
}
return 0;
}The Host Runner
void run_npc_script(const char *script_path, int npc_id) {
MogVM *vm = mog_vm_new();
mog_register_capability(vm, "game", game_functions);
mog_vm_set_global(vm);
// Player scripts get 50ms max — one game tick
MogLimits limits = { .max_cpu_ms = 50 };
mog_vm_set_limits(vm, &limits);
mog_arm_timeout(50);
int result = program_user();
if (result == MOG_INTERRUPT_CODE) {
log_warning("npc %d script timed out", npc_id);
}
mog_vm_free(vm);
}The script has access to exactly the game functions the host provides. It cannot read files, access the network, or inspect other NPCs’ internal state beyond what game exposes. The 50ms timeout prevents a buggy script from stalling the server tick.
Summary
| API Function | Purpose |
|---|---|
mog_vm_new() |
Create a VM instance |
mog_vm_set_global(vm) |
Set the global VM pointer |
mog_vm_free(vm) |
Destroy the VM |
mog_register_capability(vm, name, entries) |
Register a capability |
mog_register_posix_host(vm) |
Register built-in fs + process |
mog_validate_capabilities(vm, caps) |
Check all required capabilities exist |
mog_vm_set_limits(vm, &limits) |
Set resource limits |
mog_arm_timeout(ms) |
Arm a timeout timer |
mog_request_interrupt() |
Request script termination |
mog_cap_call(vm, cap, fn, args, n) |
Call a capability from the host |
mog_int(v), mog_string(s), … |
Construct a MogValue |
mog_arg_int(args, i), … |
Extract an argument |
The embedding model is intentionally simple: create, configure, run, destroy. The host is always in control — it decides what capabilities exist, how long scripts can run, and when to stop them. The script operates in a sandbox defined entirely by the host.
Plugins — Dynamic Loading of Mog Code
The embedding model above compiles and runs a Mog script directly. Plugins take a different approach — you compile .mog files into shared libraries (.dylib on macOS, .so on Linux) ahead of time, then load them at runtime with dlopen. The host never sees the source code. It loads a binary, queries what functions are available, and calls them.
This is the right model when you need hot-swappable logic, third-party extensions, or a modular architecture where components are developed and compiled independently.
Plugin Overview
A plugin is a pre-compiled Mog shared library. It contains:
- One or more exported functions callable from the host (Rust or C)
- Access to the host’s runtime globals (GC, value representation, etc.) via
-undefined dynamic_lookupon macOS - Metadata: plugin name, version, and export table
The key difference from direct embedding:
| Direct Embedding | Plugins | |
|---|---|---|
| Source visible to host? | Yes | No |
| Compilation happens | At load time | Ahead of time |
| Swappable at runtime? | Requires recompile | Just replace the .dylib |
| Distribution | Ship .mog files |
Ship binaries |
Use embedding when you control the scripts and want simplicity. Use plugins when you want pre-compiled, distributable, hot-swappable modules.
Writing a Plugin
A plugin is a regular .mog file. The only difference is that you mark exported functions with pub:
// math_plugin.mog — Math utilities plugin
pub fn fibonacci(n: int) -> int {
if (n <= 1) { return n; }
return fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn factorial(n: int) -> int {
if (n <= 1) { return 1; }
return n * factorial(n - 1);
}
pub fn gcd(a: int, b: int) -> int {
x := a;
y := b;
while (y != 0) {
t := x % y;
x = y;
y = t;
}
return x;
}
// Internal helper — NOT exported (no `pub` keyword)
fn square(x: int) -> int {
return x * x;
}
pub fn sum_of_squares(a: int, b: int) -> int {
return square(a) + square(b);
}pub functions become symbols in the shared library that the host can call by name. Functions without pub are compiled with hidden linkage — they exist in the binary but are not visible to the loader.
Any top-level statements in the file run during plugin initialization, before the host calls any exported function. Use this for setup work like populating lookup tables.
Compiling a Plugin
Use the mogc CLI with the --plugin flag:
# Compile a plugin to a shared library
mogc --plugin math_plugin.mog -o math_plugin.dylibThe compiler runs the full pipeline: Mog source → Rust compiler (lexer→parser→analyzer→QBE codegen) → rqbe → system assembler → system linker (with -dynamiclib). On macOS, plugins are linked with -undefined dynamic_lookup so they resolve runtime symbols (GC, VM globals, etc.) from the host process at load time rather than bundling their own copy.
The host executable must be linked with -Wl,-export_dynamic (or -Wl,-export_dynamic equivalent for your platform) to make runtime symbols visible to loaded plugins.
For an async plugin example, see examples/plugins/async_plugin_demo/.
Loading and Calling Plugins
Plugins are loaded via the C-compatible API, callable from both Rust and C. Include mog_plugin.h alongside the standard mog.h header for C hosts.
#include <stdio.h>
#include "mog.h"
#include "mog_plugin.h"
int main(int argc, char **argv) {
// Create a VM (required even for capability-free plugins)
MogVM *vm = mog_vm_new();
mog_vm_set_global(vm);
// Load the plugin
MogPlugin *plugin = mog_load_plugin("./math_plugin.dylib", vm);
if (!plugin) {
fprintf(stderr, "Failed: %s\n", mog_plugin_error());
return 1;
}
// Inspect plugin metadata
const MogPluginInfo *info = mog_plugin_get_info(plugin);
printf("Plugin: %s v%s (%lld exports)\n",
info->name, info->version, (long long)info->num_exports);
// Call exported functions
MogValue args[] = { mog_int(10) };
MogValue result = mog_plugin_call(plugin, "fibonacci", args, 1);
printf("fibonacci(10) = %lld\n", result.data.i); // 55
// Multiple arguments
MogValue gcd_args[] = { mog_int(48), mog_int(18) };
MogValue gcd_result = mog_plugin_call(plugin, "gcd", gcd_args, 2);
printf("gcd(48, 18) = %lld\n", gcd_result.data.i); // 6
// Error handling for unknown functions
MogValue bad = mog_plugin_call(plugin, "nonexistent", NULL, 0);
if (bad.tag == MOG_ERROR) {
printf("Error: %s\n", bad.data.error);
}
// Cleanup
mog_unload_plugin(plugin);
mog_vm_free(vm);
return 0;
}The pattern mirrors the embedding lifecycle: create VM, load, use, free. The difference is that mog_load_plugin replaces the compile-and-run step — the code is already compiled.
Plugin C API Reference
| Function | Purpose |
|---|---|
mog_load_plugin(path, vm) |
Load a .dylib/.so plugin |
mog_load_plugin_sandboxed(path, vm, caps) |
Load with capability allowlist |
mog_plugin_call(plugin, name, args, nargs) |
Call an exported function by name |
mog_plugin_get_info(plugin) |
Get plugin metadata (name, version, exports) |
mog_plugin_error() |
Get last error message |
mog_unload_plugin(plugin) |
Unload and free plugin |
Arguments and return values use the same MogValue tagged union described earlier. Construct arguments with mog_int(), mog_string(), etc., and inspect results through the .tag and .data fields.
Capability Sandboxing
Plugins can use requires declarations just like regular Mog programs. When a plugin declares requires fs, it means it will attempt to call filesystem functions during execution.
mog_load_plugin allows all capabilities. If you’re loading untrusted code, use mog_load_plugin_sandboxed instead — it takes a NULL-terminated allowlist of capability names. If the plugin requires a capability not on the list, loading fails immediately:
// Only allow 'log' capability — reject plugins needing 'fs', 'http', etc.
const char *allowed[] = { "log", NULL };
MogPlugin *plugin = mog_load_plugin_sandboxed("./untrusted.dylib", vm, allowed);
if (!plugin) {
// Plugin requires a capability we don't allow
fprintf(stderr, "%s\n", mog_plugin_error());
}This is the same capability model from Chapter 13, applied at load time. The plugin’s requires declarations are checked against the allowlist before any code runs. A plugin that passes the check cannot later escalate to capabilities it didn’t declare.
Plugin Protocol (Advanced)
Under the hood, the compiler generates four symbols in every plugin shared library:
mog_plugin_info()— returns a pointer to a staticMogPluginInfostruct containing the plugin name, version, and export count.mog_plugin_init(MogVM*)— initializes the GC, wires up the VM, and runs any top-level statements in the source file.mog_plugin_exports(int*)— returns an array of{name, func_ptr}pairs and writes the count to the output parameter.- Exported wrappers — each
pub fn foo(...)gets amogp_foowrapper with default (visible) linkage. Internal functions are emitted withinternallinkage so they exist in the binary but are invisible todlopen/dlsym.
Because plugins use -undefined dynamic_lookup on macOS, they share the host’s runtime globals (GC heap, VM pointer, interrupt flag). This means a plugin does not bundle its own copy of the runtime — it resolves those symbols from the host process at load time. The host must be linked with -Wl,-export_dynamic to export these symbols.
mog_load_plugin calls these in order: resolve mog_plugin_info to validate compatibility, call mog_plugin_init to set up the runtime, then call mog_plugin_exports to build the dispatch table. After that, mog_plugin_call is just a name lookup and function pointer call.
You don’t need to know any of this to use plugins. It’s documented here so you can debug issues, write tooling, or implement plugin loaders in languages other than C or Rust.
How Mog Compares to Other Embeddable Languages
If you have used other embeddable languages, here is how Mog differs:
Lua is dynamically typed, interpreted (or JIT-compiled via LuaJIT), and has a minimal core. Mog shares Lua’s philosophy of being small and embeddable, but adds static types, compiles to native code ahead of time, and enforces capability-based security rather than relying on environment sandboxing. Lua’s flexibility is a strength for interactive scripting; Mog trades that flexibility for compile-time safety and native performance.
Wren is a class-based, dynamically-typed embeddable language with a clean syntax. Like Mog, it is designed for embedding in host applications. The key differences are that Mog is statically typed, compiles ahead of time, has no classes or inheritance, and provides a formal capability model for security. Wren’s object-oriented design makes it natural for game scripting; Mog’s functional style with explicit capabilities makes it natural for agent scripting and ML workflows.
Rhai is a scripting language for Rust applications, dynamically typed, interpreted. Mog targets a similar embedding scenario but takes a different approach: static types catch errors before execution, QBE-backed compilation produces native-speed code, and the capability system provides security guarantees that a dynamic language cannot offer at compile time.
The common thread: Mog is the statically-typed, ahead-of-time-compiled option in the embeddable language space. It pays for this with a compilation step, but gains type safety, native performance, and a security model that is enforced before the script runs.
Chapter 15: Tensors — N-Dimensional Arrays
Mog provides tensors as a built-in data type — n-dimensional arrays with a fixed element dtype. They are the interchange format between Mog scripts and host-provided ML capabilities. You create tensors, read and write their elements, reshape them, and pass them to host functions. That’s it.
Tensors in Mog are deliberately not a full tensor library. There is no built-in matmul, no convolution, no autograd. The language gives you the data container; the host gives you the compute. A script describes what data to prepare and where results go. The host decides how to execute the math — CPU, GPU, or remote accelerator.
What Are Tensors?
A tensor is an n-dimensional array where every element has the same dtype. A 1D tensor is a vector. A 2D tensor is a matrix. A 3D tensor might be an image batch or a sequence of embeddings. The dimensionality is limited only by available memory.
Three properties define a tensor:
- Shape. An array of integers describing the size of each dimension. A shape of
[3, 224, 224]means 3 planes of 224×224 elements — 150,528 elements total. - Dtype. The element type. Mog supports
f16,bf16,f32, andf64. The dtype is part of the tensor’s type —tensor<f32>andtensor<f16>are different types. - Data. A flat, contiguous buffer of elements in row-major order.
Tensors are heap-allocated and garbage-collected like arrays and maps. They are passed by reference — assigning a tensor to a new variable does not copy the data.
a := tensor<f32>([3], [1.0, 2.0, 3.0]);
b := a; // b and a point to the same data
b[0] = 99.0; // a[0] is now 99.0 tooNote: If you need an independent copy, use
.reshape()with the same shape — it returns a new tensor with its own data buffer.
Creating Tensors
From a Literal
The most direct way to create a tensor is with an explicit shape and data array:
tensor<dtype>(shape, data)The dtype must be specified — there is no inference. The shape is an array of integers. The data is a flat array of values in row-major order, and its length must equal the product of the shape dimensions.
// 1D tensor (vector) — 3 elements
v := tensor<f32>([3], [1.0, 2.0, 3.0]);
// 2D tensor (matrix) — 3 rows, 4 columns
matrix := tensor<f32>([3, 4], [
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0,
]);
// 3D tensor — 2 matrices of 2x3
batch := tensor<f64>([2, 2, 3], [
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0,
10.0, 11.0, 12.0,
]);
// Scalar (0D tensor) — shape is empty
scalar := tensor<f32>([], [42.0]);If the data length doesn’t match the shape, you get a runtime error:
// Runtime error: shape [2, 3] requires 6 elements, got 4
bad := tensor<f32>([2, 3], [1.0, 2.0, 3.0, 4.0]);Static Constructors
For common initialization patterns, tensor types provide static constructors:
// All zeros
zeros := tensor<f32>.zeros([3, 224, 224]);
// All ones
ones := tensor<f32>.ones([768]);
// Random normal distribution (mean 0, stddev 1)
random := tensor<f32>.randn([10, 10]);These are the only built-in constructors. If you need other initialization patterns — linspace, arange, identity matrices — build them with a loop or request them from a host capability.
// Building an identity matrix manually
fn eye(n: int) -> tensor<f32> {
t := tensor<f32>.zeros([n, n]);
for i in 0..n {
t[(i * n) + i] = 1.0;
}
return t;
}Supported Dtypes
| Dtype | Size | Description |
|---|---|---|
f16 |
2 bytes | IEEE 754 half-precision float |
bf16 |
2 bytes | Brain floating point (truncated f32 mantissa) |
f32 |
4 bytes | IEEE 754 single-precision float |
f64 |
8 bytes | IEEE 754 double-precision float |
The dtype is part of the type system. You cannot assign a tensor<f32> to a variable of type tensor<f16> — they are different types. There is no implicit conversion between tensor dtypes.
a := tensor<f32>.zeros([10]);
b := tensor<f16>.zeros([10]);
// a = b; // Compile error: type mismatchTip: Use
f32as the default. Usef16orbf16when the host ML capability expects reduced-precision inputs — this is common for inference on GPUs. Usef64only when you need the extra precision, such as accumulating loss values over many steps.
Tensor Properties
Every tensor exposes three read-only properties:
t := tensor<f32>([3, 224, 224], /* ... */);
t.shape // [3, 224, 224] — array of ints
t.dtype // "f32" — string
t.ndim // 3 — int (same as t.shape.len)These properties are useful for validating inputs, debugging shapes, and writing generic data-processing functions:
fn describe(t: tensor<f32>) {
print("shape: {t.shape}, dtype: {t.dtype}, ndim: {t.ndim}");
total := 1;
for dim in t.shape {
total = total * dim;
}
print("total elements: {total}");
}| Property | Type | Description |
|---|---|---|
.shape |
int[] |
Array of dimension sizes |
.dtype |
string |
Element type name ("f16", "bf16", "f32", "f64") |
.ndim |
int |
Number of dimensions (equal to shape.len) |
Element Access
Tensor elements are accessed using flat indexing in row-major order. This is a single integer index into the underlying data buffer, not multi-dimensional indexing.
Reading Elements
t := tensor<f32>([2, 3], [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
// Flat index — row-major order
// Row 0: indices 0, 1, 2 → 10.0, 20.0, 30.0
// Row 1: indices 3, 4, 5 → 40.0, 50.0, 60.0
val := t[0]; // 10.0
val2 := t[4]; // 50.0The returned value type depends on the tensor’s dtype. For tensor<f32> and tensor<f64>, element access returns a float. For tensor<f16> and tensor<bf16>, the value is promoted to float on read.
Writing Elements
t := tensor<f32>([3], [0.0, 0.0, 0.0]);
t[0] = 1.0;
t[1] = 2.0;
t[2] = 3.0;
// t is now [1.0, 2.0, 3.0]Out-of-bounds access is a runtime error:
t := tensor<f32>([3], [1.0, 2.0, 3.0]);
// val := t[5]; // Runtime error: index 5 out of bounds for tensor with 3 elementsComputing Flat Indices
For multi-dimensional tensors, you compute the flat index from coordinates manually. For a tensor with shape [d0, d1, d2], the flat index of element [i, j, k] is (i * d1 * d2) + (j * d2) + k:
// 3x4 matrix
m := tensor<f32>([3, 4], [
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0,
]);
// Element at row 1, column 2 → flat index = 1*4 + 2 = 6
val := m[(1 * 4) + 2]; // 7.0
// Helper function for 2D indexing
fn idx2d(shape: int[], row: int, col: int) -> int {
return (row * shape[1]) + col;
}
val2 := m[idx2d(m.shape, 2, 3)]; // 12.0Shape Operations
Mog provides two built-in shape operations on tensors. Everything else — slicing, concatenation, padding — comes from host capabilities.
Reshape
.reshape(new_shape) returns a new tensor with the same data but a different shape. The total number of elements must be the same:
t := tensor<f32>([2, 6], [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0,
7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
]);
// Reshape 2x6 → 3x4
reshaped := t.reshape([3, 4]);
print(reshaped.shape); // [3, 4]
// Reshape to 1D
flat := t.reshape([12]);
print(flat.shape); // [12]
// Reshape to higher dimensions
cube := t.reshape([2, 2, 3]);
print(cube.shape); // [2, 2, 3]Mismatched element counts cause a runtime error:
t := tensor<f32>([2, 3], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
// bad := t.reshape([2, 4]); // Runtime error: cannot reshape 6 elements into shape [2, 4] (8 elements)Transpose
.transpose() reverses the order of dimensions. For a 2D tensor (matrix), this swaps rows and columns:
m := tensor<f32>([2, 3], [
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
]);
mt := m.transpose();
print(mt.shape); // [3, 2]
// mt data in row-major order: [1.0, 4.0, 2.0, 5.0, 3.0, 6.0]For higher-dimensional tensors, .transpose() reverses all axes. A tensor with shape [2, 3, 4] becomes [4, 3, 2]:
t := tensor<f32>.zeros([2, 3, 4]);
tt := t.transpose();
print(tt.shape); // [4, 3, 2]For 1D tensors, transpose is a no-op — it returns a tensor with the same shape.
Tensors and Host Capabilities
Tensors exist to be passed between Mog scripts and host ML capabilities. The language provides the data structure; the host provides the operations. This separation means the host can implement operations however it wants — BLAS on CPU, CUDA on GPU, or a remote inference service — without the script needing to change.
Passing Tensors to Host Functions
A host capability that performs ML operations receives tensors as arguments and returns tensors as results:
requires ml;
async fn main() -> int {
// Prepare input — e.g., a flattened 28x28 grayscale image
input := tensor<f32>.randn([1, 784]);
// Pass to host for inference
output := await ml.forward(input)?;
// Inspect the result
print("output shape: {output.shape}");
print("output dtype: {output.dtype}");
return 0;
}The .mogdecl for such a capability might look like:
capability ml {
async fn forward(input: tensor<f32>) -> tensor<f32>
async fn matmul(a: tensor<f32>, b: tensor<f32>) -> tensor<f32>
async fn relu(input: tensor<f32>) -> tensor<f32>
async fn softmax(input: tensor<f32>) -> tensor<f32>
async fn loss_mse(predicted: tensor<f32>, target: tensor<f32>) -> tensor<f32>
}Composing Operations
Since host capability calls are regular function calls that take and return tensors, you compose them naturally:
requires ml;
async fn two_layer_forward(x: tensor<f32>, w1: tensor<f32>, w2: tensor<f32>) -> Result<tensor<f32>> {
// Layer 1: matmul + relu
h := await ml.matmul(x, w1)?;
h = await ml.relu(h)?;
// Layer 2: matmul + softmax
out := await ml.matmul(h, w2)?;
out = await ml.softmax(out)?;
return ok(out);
}The script reads like a neural network definition, but every heavy operation is executed by the host. The Mog script is orchestrating — not computing.
Why This Design?
Three reasons:
- Performance. Tensor math belongs on hardware accelerators. A Mog script running on CPU should not be doing matrix multiplication — the host routes these operations to the fastest available backend.
- Safety. ML operations that allocate GPU memory, launch kernels, or talk to remote services are side effects. Keeping them in capabilities means the host controls resource usage.
- Portability. The same Mog script runs unchanged whether the host uses CPU, CUDA, Metal, or a cloud inference API. The script never names a device.
Practical Examples
Creating a Data Batch
Preparing a batch of input tensors for inference — for example, 32 flattened 28×28 images:
requires fs;
async fn load_batch(paths: string[], batch_size: int) -> tensor<f32> {
image_size := 784; // 28 * 28
batch := tensor<f32>.zeros([batch_size, image_size]);
for i in 0..batch_size {
data := await fs.read_file(paths[i])?;
// Assume data is a raw float file — parse into tensor elements
for j in 0..image_size {
batch[(i * image_size) + j] = parse_float(data, j);
}
}
return batch;
}Reading Model Output Elements
After inference, extracting results from the output tensor:
fn argmax(t: tensor<f32>) -> int {
// Find the index of the largest element (1D tensor)
best_idx := 0;
best_val := t[0];
total := t.shape[0];
for i in 1..total {
if t[i] > best_val {
best_val = t[i];
best_idx = i;
}
}
return best_idx;
}
fn top_k(t: tensor<f32>, k: int) -> int[] {
// Return indices of the k largest elements
indices := []int{};
used := map<int, bool>{};
for round in 0..k {
best_idx := -1;
best_val := -1.0e30;
total := t.shape[0];
for i in 0..total {
if !used[i] && (t[i] > best_val) {
best_val = t[i];
best_idx = i;
}
}
indices = append(indices, best_idx);
used[best_idx] = true;
}
return indices;
}Preparing Input Tensors
Normalizing raw data before passing to a model:
fn normalize(t: tensor<f32>) -> tensor<f32> {
total := t.shape[0];
// Compute mean
sum := 0.0;
for i in 0..total {
sum = sum + t[i];
}
mean := sum / float(total);
// Compute standard deviation
sq_sum := 0.0;
for i in 0..total {
diff := t[i] - mean;
sq_sum = sq_sum + (diff * diff);
}
std := sqrt(sq_sum / float(total));
// Normalize
result := tensor<f32>.zeros(t.shape);
for i in 0..total {
result[i] = (t[i] - mean) / std;
}
return result;
}End-to-End Inference Script
Putting it all together — load data, normalize, run inference, report results:
requires ml, fs;
async fn main() -> int {
// Load raw input
raw := tensor<f32>([1, 784], /* loaded from file */);
// Normalize
input := normalize(raw);
print("input shape: {input.shape}, dtype: {input.dtype}");
// Run inference
logits := await ml.forward(input)?;
probs := await ml.softmax(logits)?;
// Find prediction
prediction := argmax(probs);
confidence := probs[prediction];
print("predicted class: {prediction}");
print("confidence: {confidence}");
return 0;
}Summary
| Concept | Syntax / Example | Description |
|---|---|---|
| Create from literal | tensor<f32>([2, 3], [1.0, ...]) |
Shape + flat data in row-major order |
| Zeros | tensor<f32>.zeros([3, 224, 224]) |
All elements initialized to 0.0 |
| Ones | tensor<f32>.ones([768]) |
All elements initialized to 1.0 |
| Random normal | tensor<f32>.randn([10, 10]) |
Elements from N(0, 1) distribution |
| Shape | t.shape |
int[] — dimension sizes |
| Dtype | t.dtype |
string — "f32", "f16", etc. |
| Dimensions | t.ndim |
int — number of dimensions |
| Read element | t[i] |
Flat index, row-major order |
| Write element | t[i] = 42.0 |
Flat index assignment |
| Reshape | t.reshape([3, 4]) |
New tensor, same data, new shape |
| Transpose | t.transpose() |
Reverses dimension order |
| Supported dtypes | f16, bf16, f32, f64 |
Dtype is part of the type — no implicit conversion |
| Host ML ops | await ml.forward(t)? |
All compute goes through capabilities |
Tensors are data containers. They hold the numbers. The host provides the math. This separation keeps Mog scripts portable, safe, and small — a script that prepares tensors and calls host capabilities works the same whether the host runs on a laptop CPU or a cluster of GPUs.
Chapter 16: Advanced Topics
The previous chapters covered the core language — variables, functions, control flow, data structures, error handling, tensors, and embedding. This chapter collects the advanced features and design decisions that round out Mog: type aliases, scoped context blocks, memory layout optimizations, compilation backends, the interrupt system, garbage collection, and the things Mog deliberately leaves out.
Type Aliases
Complex types get unwieldy. A function that accepts fn(int, int) -> Result<int> is harder to read than one that accepts BinaryOp. Type aliases give names to existing types without creating new ones:
type Callback = fn(int) -> int;The alias is interchangeable with the original — no wrapping, no conversion:
type Callback = fn(int) -> int;
fn apply(f: Callback, x: int) -> int {
return f(x);
}
fn double(n: int) -> int {
return n * 2;
}
fn main() -> int {
result := apply(double, 5);
print(result); // 10
return 0;
}Aliases work with any type — scalars, collections, maps, function signatures:
type Count = int;
type Score = float;
type Matrix = [[int]];
type StringMap = {string: string};
type Predicate = fn(int) -> bool;Map type aliases are useful when a function accepts or returns configuration-style data:
type Config = {string: string};
fn default_config() -> Config {
config: Config = {};
config["mode"] = "release";
config["backend"] = "llvm";
return config;
}Function type aliases shine when callbacks appear in multiple signatures:
type Comparator = fn(int, int) -> bool;
fn sort_by(arr: [int], cmp: Comparator) -> [int] {
// sorting logic using cmp
return arr;
}
fn ascending(a: int, b: int) -> bool {
return a < b;
}
fn descending(a: int, b: int) -> bool {
return a > b;
}Aliases resolve transparently — the compiler sees through them during type checking. You cannot create a “distinct” type this way. A Count is an int, and the two are fully interchangeable.
With Blocks
Some operations need a scoped context — a mode that activates before a block and deactivates after it, regardless of how the block exits. Mog uses with blocks for this:
with no_grad() {
// gradient tracking is disabled here
output := model_forward(input);
print(output);
}
// gradient tracking resumes hereThe with keyword takes a context expression and a block. The context is entered before the block runs and exited after it completes.
Currently, no_grad() is the primary context — it disables gradient tracking during ML inference when using host tensor capabilities. This avoids unnecessary memory and computation for operations that do not need backpropagation:
// Training: gradients tracked (default)
loss := compute_loss(predictions, targets);
// Inference: no gradients needed
with no_grad() {
result := model_forward(test_input);
print(result);
}The block inside with is a normal scope. Variables declared inside are local to that block:
with no_grad() {
temp := model_forward(input);
print(temp);
}
// temp is not accessible herewith blocks can appear anywhere a statement is valid — inside functions, inside loops, nested inside other with blocks:
fn evaluate_batch(inputs: [tensor<f32>]) -> [tensor<f32>] {
results: [tensor<f32>] = [];
with no_grad() {
for i in 0..inputs.len() {
output := model_forward(inputs[i]);
results.push(output);
}
}
return results;
}Struct-of-Arrays (SoA) Performance
When you have thousands of structs and iterate over a single field, the default layout — an array of structs (AoS) — scatters that field’s values across memory. Each struct’s fields sit next to each other, so reading one field means loading every field into cache lines.
Struct-of-Arrays flips the layout. Instead of interleaving all fields per element, SoA stores each field in its own contiguous array. Iterating one field touches only that field’s memory — ideal for cache performance.
Consider a particle system:
struct Particle {
x: int,
y: int,
mass: int,
}The default approach stores particles as an array of structs:
// Array of Structs — each element has all three fields together
particles: [Particle] = [];
particles.push(Particle { x: 0, y: 0, mass: 1 });
particles.push(Particle { x: 5, y: 3, mass: 2 });The SoA approach uses the soa keyword to create a transposed layout:
// Struct of Arrays — each field stored in its own contiguous array
particles := soa Particle[10000];Access syntax is identical — particles[i].x works the same way. The difference is in memory layout, not in your code:
// Initialize positions
for i in 0..10000 {
particles[i].x = i;
particles[i].y = i * 2;
particles[i].mass = 1;
}
// Update all x values — contiguous memory access, cache-friendly
for i in 0..10000 {
particles[i].x = particles[i].x + 1;
}When should you use SoA? When you iterate over many elements and touch only one or two fields at a time. Physics simulations, particle systems, columnar data processing — these benefit from SoA. When you access all fields of each element together, regular arrays of structs are fine.
struct Star {
brightness: float,
temperature: float,
distance: float,
name: string,
}
// SoA: good when filtering by one field
stars := soa Star[50000];
// This loop only touches 'brightness' — contiguous reads
for i in 0..50000 {
if stars[i].brightness > 5.0 {
print(i);
}
}The capacity is fixed at creation. soa Star[50000] allocates space for exactly 50,000 elements. This is a deliberate tradeoff — fixed size enables the compiler to lay out memory optimally and elide bounds checks in release builds.
Compilation Backend: rqbe
Mog compiles to native ARM64 and x86 binaries through rqbe, a safe Rust implementation of the QBE backend. rqbe runs entirely in-process inside the mogc compiler — no external code generation tools are needed.
The compilation pipeline is: Mog source → Rust compiler frontend (lexer → parser → analyzer → QBE codegen) → rqbe (in-process, safe Rust) → system assembler → system linker. The frontend and backend are a single Rust binary (mogc), built with cargo build --release --manifest-path compiler/Cargo.toml.
rqbe focuses on correctness and fast compile times over peak optimization. It does not apply aggressive optimizations like inlining or loop vectorization, but it produces correct, reasonably efficient native code. For the short-lived embedded scripts Mog targets, compile speed matters more than extracting the last percent of runtime performance.
| Property | rqbe |
|---|---|
| Language | Safe Rust (in-process) |
| Compile speed | Fast — milliseconds for typical scripts |
| Runtime performance | Good, not aggressively optimized |
| Target architectures | ARM64, x86 |
| External dependencies | System assembler + linker only |
The generated code links against the same C runtime library (which provides the garbage collector, tensor operations, async runtime, and host bindings).
The Interrupt System
Mog scripts run inside a host application. The host needs a way to stop long-running or runaway scripts — a tight loop that never yields, an accidental infinite recursion, or simply a user pressing cancel.
Mog uses cooperative interrupt polling. The compiler inserts a check at every loop back-edge — the point where a while, for, or for-each loop jumps back to its condition. At each check, the script reads a volatile global flag. If the flag is set, the script exits immediately.
From the host side, two C functions control interrupts:
// Request that the running script stop
void mog_request_interrupt(void);
// Arm an automatic timeout — interrupt fires after ms milliseconds
void mog_arm_timeout(int ms);mog_request_interrupt() sets the flag directly. The script will stop at the next loop back-edge — usually within microseconds for any loop-heavy code.
mog_arm_timeout(ms) spawns a background thread that sleeps for the given duration, then sets the interrupt flag. This is useful for enforcing time limits on untrusted scripts:
// Give the script 5 seconds
mog_arm_timeout(5000);
mog_run_script(vm, script);
mog_cancel_timeout(); // cancel if script finished earlyThe overhead of interrupt checking is small — roughly 1–3% in loop-heavy benchmarks. Each check is a single volatile load and a branch, which modern CPUs predict correctly almost every time (the flag is almost always zero).
The interrupt flag can also be cleared:
void mog_clear_interrupt(void);
int mog_interrupt_requested(void);This lets a host reset the flag between running multiple scripts, or check whether a script was interrupted versus completed normally.
Every loop type is covered — while, for with ranges, for-each over collections. Nested loops get independent checks. There is no way for a Mog script to disable or bypass interrupt polling.
Memory Management
Mog is garbage collected. All heap allocations — structs, arrays, maps, strings, closures, tensors, SoA containers — go through the garbage collector. There is no manual free, no RAII, no reference counting.
The GC uses a mark-and-sweep algorithm with a shadow stack for root tracking:
- Allocation.
gc_allocrequests memory from the GC. If the allocation count exceeds a threshold, a collection cycle runs first. - Root tracking. Each function call pushes a GC frame (
gc_push_frame). Local variables that hold heap pointers are registered as roots in the current frame (gc_add_root). When the function returns, the frame is popped (gc_pop_frame). - Collection. The collector walks the shadow stack, marks all reachable objects, then sweeps unmarked objects and frees their memory. The allocation threshold grows after each cycle to avoid excessive collection.
This is an implementation detail you rarely need to think about. You allocate by creating values — the language handles the rest:
fn make_particles(n: int) -> [Particle] {
particles: [Particle] = [];
for i in 0..n {
// Each Particle is GC-allocated; no manual cleanup needed
particles.push(Particle { x: i, y: i * 2, mass: 1 });
}
return particles;
}
// When particles is no longer reachable, the GC reclaims the memoryClosures capture variables by value (see Chapter 6), and the captured environment is itself a GC-allocated block:
fn make_counter(start: int) -> fn() -> int {
count := start;
return fn() -> int {
count = count + 1;
return count;
};
}
// The closure's captured 'count' lives on the GC heapThere are no weak references, no finalizers, and no way to manually trigger collection from Mog code. The GC is non-generational and non-concurrent — it stops the script during collection. For the short-lived scripts Mog targets, this is a reasonable tradeoff: simplicity and correctness over pause-time optimization.
What Mog Does NOT Have
Mog’s design is subtractive. Every feature must justify its complexity, and many common language features did not make the cut. This is intentional — a smaller language is easier to learn, easier to embed, and easier to reason about.
No generics (beyond tensor<dtype>, Result<T>, and ?T). Mog has a small, fixed set of parameterized types built into the language. You cannot define your own generic structs or functions. This eliminates an entire class of complexity — no type parameter inference, no trait bounds, no monomorphization. If you need a collection of a specific type, you use the built-in array, map, or struct types directly.
No classes, inheritance, interfaces, or traits. Mog has structs with fields and standalone functions. There is no method dispatch, no vtables, no subtyping hierarchy. If you need polymorphism, use closures — a fn(int) -> int doesn’t care which function it points to.
No operator overloading. + means numeric addition or string concatenation. It cannot be redefined for custom types. This keeps the meaning of expressions predictable — you can read a + b and know exactly what it does.
No macros or metaprogramming. No compile-time code generation, no syntax extensions, no preprocessor. The language you read is the language that runs. This makes Mog code uniformly readable — there are no project-specific DSLs hiding behind macro expansions.
No exceptions. Error handling uses Result<T> and the ? propagation operator (see Chapter 10). Errors are values, not control flow. Every function that can fail says so in its return type, and the compiler enforces that you handle the error or propagate it.
No null. Mog uses ?T (Optional) for values that might be absent — ?int, ?string (see Chapter 10). The type system distinguishes between “definitely has a value” and “might not have a value.” You cannot accidentally dereference something that does not exist.
No raw pointers or manual memory management. All memory is GC-managed. You cannot take the address of a variable, cast between pointer types, or free memory. This eliminates use-after-free, double-free, buffer overflows, and dangling pointers by construction.
No implicit type coercion. An int does not silently become a float. A float does not silently become a string. All conversions are explicit function calls — float_from_string, int(x), and so on. This prevents an entire category of subtle bugs where silent coercion produces unexpected results.
No standalone execution. Mog scripts run inside a host application. There is no mog run file.mog command that produces a self-contained process with filesystem access, network sockets, or an event loop. The host provides capabilities, and the script declares which ones it needs (see Chapter 13). This is the core security model — a Mog script can only do what its host explicitly permits.
These omissions are not gaps to be filled in future versions. They are design decisions that keep Mog small, predictable, and safe for embedding. A language that tries to be everything ends up being harder to trust. Mog trades breadth for clarity.
Chapter 17: Cookbook — Practical Programs
This chapter is a collection of complete, runnable Mog programs. Each one demonstrates a combination of language features in a realistic context — loops, structs, closures, error handling, async, capabilities. Read them in order or jump to whichever looks interesting.
1. FizzBuzz
The classic interview problem: print numbers 1 to 100, but replace multiples of 3 with “Fizz”, multiples of 5 with “Buzz”, and multiples of both with “FizzBuzz”.
fn fizzbuzz(n: int) -> string {
divisible_by_3 := (n % 3) == 0;
divisible_by_5 := (n % 5) == 0;
if divisible_by_3 && divisible_by_5 {
return "FizzBuzz";
} else if divisible_by_3 {
return "Fizz";
} else if divisible_by_5 {
return "Buzz";
}
return str(n);
}
fn main() -> int {
for i := 1 to 100 {
println(fizzbuzz(i));
}
return 0;
}The for i := 1 to 100 loop is inclusive on both ends, so it covers exactly 1 through 100. The str() builtin converts an integer to its string representation.
2. Fibonacci Sequence
Two approaches to computing Fibonacci numbers: a clean recursive version and a fast iterative version. Both print the first 20 values.
Recursive
fn fib(n: int) -> int {
if n <= 1 {
return n;
}
return fib(n - 1) + fib(n - 2);
}
fn main() -> int {
for i in 0..20 {
println(f"fib({i}) = {fib(i)}");
}
return 0;
}This is the textbook definition. It’s simple but exponentially slow — fib(40) would take a noticeable pause. Fine for small inputs and clarity.
Iterative
fn fib_iter(n: int) -> int {
if n <= 1 {
return n;
}
a := 0;
b := 1;
for i in 2..(n + 1) {
temp := a + b;
a = b;
b = temp;
}
return b;
}
fn main() -> int {
results: [string] = [];
for i in 0..20 {
results.push(str(fib_iter(i)));
}
println(results.join(", "));
return 0;
}Output:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181The iterative version runs in linear time. The range 2..(n + 1) is half-open, so it runs from 2 through n inclusive — exactly n - 1 iterations, which is all you need.
3. Word Frequency Counter
Count how often each word appears in a list. This uses a map as a frequency table and demonstrates map creation, key checking, and iteration.
fn count_words(words: [string]) -> {string: int} {
counts: {string: int} = {};
for word in words {
if counts.has(word) {
counts[word] = counts[word] + 1;
} else {
counts[word] = 1;
}
}
return counts;
}
fn main() -> int {
words := [
"the", "quick", "brown", "fox", "jumps", "over", "the", "lazy",
"dog", "the", "fox", "jumps", "high", "over", "the", "fence",
"and", "the", "dog", "sleeps", "under", "the", "fence",
];
freq := count_words(words);
// Collect into an array of formatted strings for sorting
entries: [string] = [];
for word, count in freq {
entries.push(f"{count}\t{word}");
}
entries.sort(fn(a: string, b: string) -> int {
if a > b { return -1; }
if a < b { return 1; }
return 0;
});
println("Word frequencies (descending):");
for entry in entries {
println(f" {entry}");
}
return 0;
}The empty map {} needs a type annotation because Mog can’t infer the value type from an empty literal. Sorting the formatted strings lexicographically (descending) puts higher counts first since the count is the leading character.
4. Simple Calculator with Error Handling
A calculator function that takes an operator string and two numbers, returning a Result<int>. This demonstrates how Result communicates failure through return types instead of exceptions.
fn calculate(op: string, a: int, b: int) -> Result<int> {
match op {
"+" => return ok(a + b),
"-" => return ok(a - b),
"*" => return ok(a * b),
"/" => {
if b == 0 {
return err("division by zero");
}
return ok(a / b);
},
"%" => {
if b == 0 {
return err("modulo by zero");
}
return ok(a % b);
},
_ => return err(f"unknown operator: {op}"),
}
}
fn eval_and_print(op: string, a: int, b: int) {
match calculate(op, a, b) {
ok(result) => println(f" {a} {op} {b} = {result}"),
err(e) => println(f" {a} {op} {b} => ERROR: {e}"),
}
}
fn main() -> int {
println("Calculator:");
eval_and_print("+", 10, 3);
eval_and_print("-", 10, 3);
eval_and_print("*", 10, 3);
eval_and_print("/", 10, 3);
eval_and_print("/", 10, 0);
eval_and_print("%", 10, 3);
eval_and_print("^", 2, 8);
return 0;
}Output:
Calculator:
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3
10 / 0 => ERROR: division by zero
10 % 3 = 1
2 ^ 8 => ERROR: unknown operator: ^Every call site decides how to handle the error. The match on calculate’s return value forces the caller to acknowledge both cases. Nothing fails silently.
5. Data Validation Chain
Validate user input using a chain of Result-returning functions. The ? operator propagates the first failure, short-circuiting the rest of the chain.
struct UserInput {
username: string,
email: string,
age: int,
}
struct ValidUser {
username: string,
email: string,
age: int,
}
fn validate_username(name: string) -> Result<string> {
if name.len() == 0 {
return err("username cannot be empty");
}
if name.len() < 3 {
return err(f"username too short: {name.len()} chars (minimum 3)");
}
if name.len() > 20 {
return err(f"username too long: {name.len()} chars (maximum 20)");
}
return ok(name);
}
fn validate_email(email: string) -> Result<string> {
if email.len() == 0 {
return err("email cannot be empty");
}
if !email.contains("@") {
return err(f"invalid email (missing @): {email}");
}
if !email.contains(".") {
return err(f"invalid email (missing domain): {email}");
}
return ok(email);
}
fn validate_age(age: int) -> Result<int> {
if age < 0 {
return err("age cannot be negative");
}
if age < 13 {
return err(f"must be at least 13 years old (got {age})");
}
if age > 150 {
return err(f"age seems unrealistic: {age}");
}
return ok(age);
}
fn validate_user(input: UserInput) -> Result<ValidUser> {
name := validate_username(input.username)?;
email := validate_email(input.email)?;
age := validate_age(input.age)?;
return ok(ValidUser { username: name, email: email, age: age });
}
fn main() -> int {
inputs := [
UserInput { username: "alice", email: "alice@example.com", age: 30 },
UserInput { username: "", email: "bob@test.com", age: 25 },
UserInput { username: "charlie", email: "not-an-email", age: 17 },
UserInput { username: "dana", email: "dana@corp.io", age: 8 },
];
for i, input in inputs {
match validate_user(input) {
ok(user) => println(f"[{i}] Valid: {user.username} ({user.email}), age {user.age}"),
err(e) => println(f"[{i}] Invalid: {e}"),
}
}
return 0;
}Output:
[0] Valid: alice (alice@example.com), age 30
[1] Invalid: username cannot be empty
[2] Invalid: invalid email (missing @): not-an-email
[3] Invalid: must be at least 13 years old (got 8)Each validator is a small, testable function. The ? in validate_user means the function returns the first error it hits — you never get a confusing cascade of failures. If you wanted to collect all errors instead, you’d call each validator independently and accumulate the results.
6. Sorting and Filtering Pipeline
Create an array of structs, filter by a condition, sort the results, and display them. This is the bread-and-butter pattern for data processing in Mog.
struct Employee {
name: string,
department: string,
salary: int,
years: int,
}
fn main() -> int {
staff := [
Employee { name: "Alice", department: "engineering", salary: 120000, years: 5 },
Employee { name: "Bob", department: "marketing", salary: 85000, years: 3 },
Employee { name: "Charlie", department: "engineering", salary: 135000, years: 8 },
Employee { name: "Dana", department: "design", salary: 95000, years: 4 },
Employee { name: "Eve", department: "engineering", salary: 110000, years: 2 },
Employee { name: "Frank", department: "marketing", salary: 78000, years: 1 },
Employee { name: "Grace", department: "design", salary: 105000, years: 6 },
Employee { name: "Hank", department: "engineering", salary: 145000, years: 10 },
];
// Find engineers with 3+ years, sorted by salary descending
senior_engineers := staff
.filter(fn(e: Employee) -> bool { (e.department == "engineering") && (e.years >= 3) })
.sort(fn(a: Employee, b: Employee) -> int { b.salary - a.salary });
println("Senior Engineers (3+ years, by salary):");
for e in senior_engineers {
println(f" {e.name} — ${e.salary}/yr, {e.years} years");
}
// Compute total payroll by department
departments := ["engineering", "marketing", "design"];
for dept in departments {
total := 0;
count := 0;
for e in staff {
if e.department == dept {
total = total + e.salary;
count = count + 1;
}
}
avg := total / count;
println(f"{dept}: {count} people, ${total} total, ${avg} avg");
}
return 0;
}The .filter().sort() chain reads top-to-bottom: first narrow the dataset, then order it. The comparator b.salary - a.salary sorts descending because a positive result means b should come first.
7. Matrix Operations
Multiply two matrices represented as nested arrays. This demonstrates nested loops and 2D array construction.
fn matrix_multiply(a: [[int]], b: [[int]]) -> Result<[[int]]> {
rows_a := a.len();
if rows_a == 0 {
return err("matrix A is empty");
}
cols_a := a[0].len();
rows_b := b.len();
if cols_a != rows_b {
return err(f"dimension mismatch: A is {rows_a}x{cols_a}, B is {rows_b}x{b[0].len()}");
}
cols_b := b[0].len();
// Build result matrix filled with zeros
result: [[int]] = [];
for i in 0..rows_a {
row := [0; cols_b];
result.push(row);
}
// Multiply
for i in 0..rows_a {
for j in 0..cols_b {
sum := 0;
for k in 0..cols_a {
sum = sum + (a[i][k] * b[k][j]);
}
result[i][j] = sum;
}
}
return ok(result);
}
fn print_matrix(label: string, m: [[int]]) {
println(f"{label}:");
for row in m {
parts: [string] = [];
for val in row {
parts.push(str(val));
}
println(f" [{parts.join(", ")}]");
}
}
fn main() -> int {
a := [
[1, 2, 3],
[4, 5, 6],
];
b := [
[7, 8],
[9, 10],
[11, 12],
];
print_matrix("A (2x3)", a);
print_matrix("B (3x2)", b);
match matrix_multiply(a, b) {
ok(c) => print_matrix("A × B", c),
err(e) => println(f"Error: {e}"),
}
return 0;
}Output:
A (2x3):
[1, 2, 3]
[4, 5, 6]
B (3x2):
[7, 8]
[9, 10]
[11, 12]
A × B:
[58, 64]
[139, 154]The [0; cols_b] syntax creates a pre-filled array of zeros. The function returns Result because the caller might pass matrices with incompatible dimensions — better to report the error than to crash with an out-of-bounds access.
8. Agent Tool-Use Script
This is the primary Mog use case: a script generated by an LLM agent that uses host-provided capabilities to accomplish a real task. It reads files, runs a shell command, checks environment variables, and writes a report — all through capabilities the host explicitly grants.
requires fs, process;
optional log;
struct FileInfo {
path: string,
size: int,
extension: string,
}
fn get_extension(path: string) -> string {
parts := path.split(".");
if parts.len() < 2 {
return "";
}
return parts[parts.len() - 1];
}
async fn scan_directory(dir: string) -> Result<[FileInfo]> {
log.info(f"scanning directory: {dir}");
entries := await fs.list_dir(dir)?;
files: [FileInfo] = [];
for entry in entries {
path := f"{dir}/{entry}";
if await fs.is_file(path)? {
size := await fs.file_size(path)?;
files.push(FileInfo {
path: path,
size: size,
extension: get_extension(entry),
});
}
}
return ok(files);
}
async fn generate_report(files: [FileInfo]) -> Result<string> {
// Group sizes by extension
ext_sizes: {string: int} = {};
ext_counts: {string: int} = {};
total_size := 0;
for f in files {
ext := if f.extension.len() > 0 { f.extension } else { "(none)" };
if ext_sizes.has(ext) {
ext_sizes[ext] = ext_sizes[ext] + f.size;
ext_counts[ext] = ext_counts[ext] + 1;
} else {
ext_sizes[ext] = f.size;
ext_counts[ext] = 1;
}
total_size = total_size + f.size;
}
lines: [string] = [];
lines.push(f"Directory Report");
lines.push(f"================");
lines.push(f"Total files: {files.len()}");
lines.push(f"Total size: {total_size} bytes");
lines.push(f"");
lines.push(f"By extension:");
for ext, size in ext_sizes {
count := ext_counts[ext];
lines.push(f" .{ext}: {count} files, {size} bytes");
}
// Find largest files
sorted := files.sort(fn(a: FileInfo, b: FileInfo) -> int { b.size - a.size });
lines.push(f"");
lines.push(f"Largest files:");
limit := if sorted.len() < 5 { sorted.len() } else { 5 };
for i in 0..limit {
lines.push(f" {sorted[i].path} ({sorted[i].size} bytes)");
}
return ok(lines.join("\n"));
}
async fn main() -> int {
dir := process.getenv("SCAN_DIR");
if dir.len() == 0 {
dir = process.cwd();
}
log.info(f"starting scan of {dir}");
match await scan_directory(dir) {
ok(files) => {
println(f"Found {files.len()} files");
match await generate_report(files) {
ok(report) => {
println(report);
await fs.write_file("report.txt", report)?;
log.info("report written to report.txt");
},
err(e) => println(f"Report generation failed: {e}"),
}
},
err(e) => println(f"Scan failed: {e}"),
}
return 0;
}A few things to notice:
requires fs, process;declares the capabilities this script needs. The host must provide both, or the script won’t run.optional log;means the script can function without logging — calls tolog.info()are silently ignored if the host doesn’t provide thelogcapability.- Every filesystem operation is
asyncand returnsResult, so the script usesawaitand?together:await fs.read_file(path)?. - The
mainfunction isasync fn main()because it calls async capability functions.
9. Recursive Tree Traversal
Build a tree out of structs and traverse it with recursion. This demonstrates self-referential structs, recursive functions, and accumulating results.
struct TreeNode {
label: string,
value: int,
children: [TreeNode],
}
fn tree_sum(node: TreeNode) -> int {
total := node.value;
for child in node.children {
total = total + tree_sum(child);
}
return total;
}
fn tree_depth(node: TreeNode) -> int {
if node.children.len() == 0 {
return 1;
}
max_child := 0;
for child in node.children {
d := tree_depth(child);
if d > max_child {
max_child = d;
}
}
return max_child + 1;
}
fn tree_find(node: TreeNode, target: string) -> ?TreeNode {
if node.label == target {
return some(node);
}
for child in node.children {
match tree_find(child, target) {
some(found) => return some(found),
none => {},
}
}
return none;
}
fn print_tree(node: TreeNode, indent: int) {
prefix := "";
for i in 0..indent {
prefix = prefix + " ";
}
println(f"{prefix}{node.label} ({node.value})");
for child in node.children {
print_tree(child, indent + 1);
}
}
fn main() -> int {
tree := TreeNode {
label: "root", value: 1,
children: [
TreeNode {
label: "math", value: 10,
children: [
TreeNode { label: "algebra", value: 3, children: [] },
TreeNode { label: "calculus", value: 7, children: [] },
],
},
TreeNode {
label: "science", value: 20,
children: [
TreeNode {
label: "physics", value: 5,
children: [
TreeNode { label: "optics", value: 2, children: [] },
TreeNode { label: "mechanics", value: 4, children: [] },
],
},
TreeNode { label: "chemistry", value: 8, children: [] },
],
},
TreeNode { label: "art", value: 15, children: [] },
],
};
println("Tree structure:");
print_tree(tree, 0);
println(f"\nTotal value: {tree_sum(tree)}");
println(f"Max depth: {tree_depth(tree)}");
match tree_find(tree, "physics") {
some(node) => println(f"Found '{node.label}' with value {node.value}"),
none => println("Not found"),
}
match tree_find(tree, "history") {
some(node) => println(f"Found '{node.label}'"),
none => println("'history' not found in tree"),
}
return 0;
}Output:
Tree structure:
root (1)
math (10)
algebra (3)
calculus (7)
science (20)
physics (5)
optics (2)
mechanics (4)
chemistry (8)
art (15)
Total value: 75
Max depth: 4
Found 'physics' with value 5
'history' not found in treeThree recursive functions work the tree: tree_sum adds all values, tree_depth finds the longest path, and tree_find searches by label and returns ?TreeNode. The tree_find function shows how ? return types compose with recursion — each level either returns some(found) or continues searching.
10. Async Pipeline
Chain multiple async operations together, each depending on the previous result. This demonstrates await, ? propagation through async calls, and parallel execution with all().
requires http, fs;
optional log;
struct ApiResult {
source: string,
data: string,
status: int,
}
async fn fetch_json(url: string) -> Result<ApiResult> {
log.info(f"fetching {url}");
response := await http.get(url)?;
if response.status != 200 {
return err(f"HTTP {response.status} from {url}");
}
return ok(ApiResult {
source: url,
data: response.body,
status: response.status,
});
}
async fn fetch_all_sources(urls: [string]) -> Result<[ApiResult]> {
// Build futures for parallel execution
futures: [Result<ApiResult>] = [];
for url in urls {
futures.push(fetch_json(url));
}
results := await all(futures)?;
log.info(f"fetched {results.len()} sources");
return ok(results);
}
fn merge_results(results: [ApiResult]) -> string {
lines: [string] = [];
for r in results {
lines.push(f"--- Source: {r.source} (HTTP {r.status}) ---");
lines.push(r.data);
lines.push("");
}
return lines.join("\n");
}
async fn process_and_save(urls: [string], output_path: string) -> Result<int> {
// Step 1: Fetch all sources in parallel
results := await fetch_all_sources(urls)?;
// Step 2: Filter out empty responses
valid := results.filter(fn(r: ApiResult) -> bool { r.data.len() > 0 });
log.info(f"{valid.len()} of {results.len()} sources returned data");
if valid.len() == 0 {
return err("all sources returned empty data");
}
// Step 3: Merge into a single document
merged := merge_results(valid);
// Step 4: Write to disk
await fs.write_file(output_path, merged)?;
log.info(f"wrote {merged.len()} bytes to {output_path}");
return ok(valid.len());
}
async fn main() -> int {
urls := [
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/inventory",
];
match await process_and_save(urls, "combined_data.txt") {
ok(count) => println(f"Pipeline complete: {count} sources merged"),
err(e) => {
println(f"Pipeline failed: {e}");
return 1;
},
}
return 0;
}The pipeline flows top-to-bottom: fetch in parallel, filter, merge, write. Each step either succeeds and feeds into the next, or fails and the error propagates up through ?. The await all(futures)? line does two things — it waits for all fetches to complete in parallel, then unwraps the combined result, failing if any single fetch failed.
Note: Returning a nonzero exit code from
mainsignals failure to the host. Thereturn 1;in the error branch lets the host know the script didn’t complete successfully.
Summary
| Program | Key Features |
|---|---|
| FizzBuzz | for loop, if/else, str() |
| Fibonacci | Recursion, iterative accumulation, 0..n range |
| Word Frequency | Maps, .has(), for key, value in map |
| Calculator | Result<T>, match on result, ok() / err() |
| Validation Chain | ? propagation, composing Result functions |
| Sort & Filter | Structs, .filter(), .sort(), closures |
| Matrix Multiply | Nested arrays, [0; n] fill, triple nested loop |
| Agent Script | requires/optional, async/await, capabilities |
| Tree Traversal | Recursive structs, ?T optional, depth-first search |
| Async Pipeline | await all(), chained async steps, error propagation |
These programs cover the core of practical Mog: data processing with arrays and maps, error handling with Result and ?, struct-based domain modeling, recursive algorithms, and async capability-driven scripts. Each pattern scales — the validation chain works for 3 fields or 30, the async pipeline works for 3 URLs or 300.