The Magister Catalyst framework

Magister Catalyst
User's guide

Magister Catalyst, subsequently referred to as Catalyst, provides a framework for building native applications. This guide explains Catalyst values, CSON descriptions, dates and times, SQL databases, shell commands, and asynchronous messaging, with examples you can adapt to your application.

The SDK reference lists every header and its methods, with concise notes on behavior, ownership, and defaults.

Install the SDK

The Catalyst SDK

The proprietary Catalyst SDK is distributed as a versioned zip for macOS on Apple Silicon. Extract it anywhere, set CMAKE_PREFIX_PATH to that directory, and use find_package(Catalyst CONFIG REQUIRED). The archive includes mc headers, shared libraries and Mac frameworks, Cosmic, Microcosm, Nexus, Python and Swift bindings, runtime dependencies, documentation, examples, resources, and configuration. Its CMake package links your application to the supplied libraries.

See the SDK instructions for setup, framework integration, and application deployment. The minimum macOS version is recorded in distribution.json. Xcode or Command Line Tools supplies the platform SDK; Metal compilation also requires Xcode's Metal Toolchain. Linux support is coming soon.

The included Python package uses CPython's stable abi3 interface (3.9 or newer). Add the SDK's lib/python directory to PYTHONPATH. The Swift package in share/Catalyst/swift compiles its wrapper sources with Swift 5.9 or newer and links to the bundled native library. Both expose cvar, CMessenger, and CServer, with their support types. See the binding guide and SDK instructions for integration details.

The SDK includes the compilers and dependencies needed to build Catalyst applications. CMake configures the bundled headers and libraries automatically; no separate Boost or LLVM installation is needed. Standard C++ projects use Apple's selected compiler.

Configure your environment

Set CATALYST_SDK to the extracted SDK directory. Add its commands to PATH and set MC_HOME so applications can find its configuration and resources:

export CATALYST_SDK="$HOME/SDKs/Catalyst-1.0.0-macos-arm64"
export PATH="$CATALYST_SDK/bin:$PATH"
export MC_HOME="$CATALYST_SDK"

For GPU programs, install Xcode's Metal Toolchain if it is not already available:

xcodebuild -downloadComponent MetalToolchain
xcrun -sdk macosx metal --version

Build your application

To use an installed Catalyst SDK from a CMake project:

cmake_minimum_required(VERSION 3.24)
project(MyApplication LANGUAGES CXX)
find_package(Catalyst CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE Catalyst::Shared)
catalyst_debug_symbols(my_app)

Save this as CMakeLists.txt beside main.cpp, then configure and build your application:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_PREFIX_PATH="$CATALYST_SDK"
cmake --build build

Catalyst::Shared links the supplied core library and provides its headers and dependencies. Use this target for applications using cvar or other compiled framework facilities. Catalyst::Catalyst supplies just the headers and their dependencies for header-only use. The SDK provides shared libraries and Mac frameworks.

The C++ API uses namespace mc and headers such as <mc/cvar.h>.

The C binding header is <mc/CCatalyst.h>. Its runtime entry points include cCatalystABIVersion() and cCatalystDrain(). The Swift package uses the CCatalyst C module; Python applications import catalyst.

<mc/core.h> is a convenience header for common Catalyst facilities, including containers, parsing, threads, pools, queues, commands, and messaging. It also provides short type aliases such as i8, f8, and Strings, and makes the std and mc namespaces available without qualification. Include individual headers when you want to keep names qualified.

The SDK also provides <mc/pool.h>, <mc/queue.h>, <mc/CCommand.h>, <mc/CMessenger.h>, and <mc/CServer.h> individually. Pool, queue, and command APIs are header-only. Link messaging applications with Catalyst::Shared.

<mc/image.h> provides JPEG encoding/decoding and pixel-buffer placement. These functions belong to libcatalyst and are available independently of the macOS drawing library.

CDB requires PostgreSQL 15 or newer; PostgreSQL runs as a separate service. Install a server locally for development, or connect to an existing PostgreSQL server. To start a new local service and create an empty example database:

brew install postgresql@18
brew services start postgresql@18
"$(brew --prefix postgresql@18)/bin/createdb" catalyst_demo

If PostgreSQL already runs on your machine, use that service or configure a different port for the new one. These examples use a new database named catalyst_demo.

Run the tutorials

The SDK's examples/ directory contains 33 commented Cosmic tutorials. Each program checks its results, including when built with optimization. Configure the supplied CMake project, then build and run the lessons:

cmake -S "$CATALYST_SDK/examples" -B examples-build \
  -DCMAKE_PREFIX_PATH="$CATALYST_SDK"
cmake --build examples-build --target catalyst_examples
./examples-build/example-values
./examples-build/example-program -name Ada -repeat 2

See examples/README.md in the SDK for the reading order, suggested experiments, and service requirements. Start with example-cosmic-syntax for inferred declarations, concise functions, and collection loops. The database lesson covers generated rows and queries; the compute lesson combines CPU and GPU functions in one Cosmic source file. Graphics and compute require macOS, and GPU execution requires an available Metal device.

The HTTP and server lessons supply their own local peers. The WebSocket, messenger, and PostgreSQL client lessons require a suitable service as described in the examples' README. Application options such as -name use a single hyphen.

Minimal program

Save this as main.cpp. It creates a CProgram from the command-line arguments and a null cvar, then exits successfully without printing anything:

#include <mc/CProgram.h>
#include <mc/cvar.h>

int main(int argc, char** argv){
  mc::CProgram program(argc, argv);
  mc::cvar value;
  return 0;
}

Build it with the CMakeLists.txt and commands in SDK setup, then run it with MC_HOME set to the extracted SDK:

MC_HOME="$CATALYST_SDK" ./build/my_app

CProgram defaults cArgs().at("#name") to cBasename(cExecutablePath()), including any filename extension. Configuration lookup uses that actual executable name even when launched through a symbolic link or a substituted argv[0]. Positional argument zero retains the original invocation text. --name chosen selects chosen.ccfg explicitly; constructors taking a cvar argument map also accept "#name". Reconfiguration retains the selected name across executable upgrades.

Errors and stack traces

In both Release and Debug builds, CError remembers the stack where the error was constructed. After your application initializes CProgram, an uncaught CError prints its message and that stack to standard error before the process terminates. This includes errors escaping a worker thread and errors during CProgram initialization.

#include <mc/CProgram.h>
#include <mc/error.h>

void loadSettings(){
  throw mc::CError("settings file is missing");
}

int main(int argc, char** argv){
  mc::CProgram program(argc, argv);
  loadSettings();
}

A Debug build with source information produces output like this (addresses and additional system frames are omitted here):

Uncaught mc::CError: settings file is missing
Stack trace (most recent call first):
#0 loadSettings() at /path/to/main.cpp:5
#1 main at /path/to/main.cpp:10

Copies and rethrows retain the original construction stack. Construct an error where the failure occurs: throwing an error object created earlier reports that earlier location. Catalyst errors you catch are silent unless you explicitly log them. To report a caught error:

#include <iostream>

try{
  loadSettings();
}
catch(const mc::CError& error){
  std::cerr << error.what() << '\n' << error.stackTrace();
}

what() and str() still return only the message. stackTrace() returns a cstr, or an empty string when capture is disabled or a trace cannot be obtained. Diagnostic failures do not replace the original error. To inspect the current call stack instead, use CProgram::stackTrace().

Stack capture does not require -g or debug-symbol bundles. The SDK's optimized Release libraries report available function symbols and addresses, without framework source code or file-and-line information. Optimization can inline or remove calls, and stripped functions may appear only as addresses or offsets from the nearest available symbol.

Build with source information

For your application, configure CMake with -DCMAKE_BUILD_TYPE=Debug and call catalyst_debug_symbols(my_app) after creating the target. Call it for each of your shared-library targets too. On macOS this generates matching .dSYM bundles; keep them beside their binaries when copying or installing your application.

The SDK contains optimized Release libraries without debug symbols. Debugging your application does not add source information to those libraries. Your application's frames can include source file names and line numbers while framework frames continue to show symbols and addresses.

MC_ERROR_STACK_TRACES defaults to 1 regardless of NDEBUG. Keep this setting consistent across the application and its libraries; the SDK uses the enabled default. For custom macOS build systems, generate matching debug bundles for your executables and shared libraries when you want source locations in their traces.

Scope and limitations

Traces retain up to 64 stack addresses; very deep stacks are truncated. Compiler optimization and missing symbols can reduce the available frames or source detail. When Catalyst translates an external exception to CError, its trace starts at that translation; it cannot recover calls already unwound by the external exception.

CProgram installs its reporting handler on first use of its program services, including option registration. It calls the previous terminate handler after reporting, so an existing crash reporter or the C++ runtime may print additional diagnostics. A later call to std::set_terminate() replaces Catalyst's handler. Arbitrary C++ exceptions and fatal signals do not acquire saved CError traces.

Priority queues

CPriorityQueue<Item> is a bounded, lock-free queue for multiple producers and consumers. Submit a value and a double priority with push(item, priority); pop(item) removes the highest-priority value currently queued. Use pop(item, priority) to retrieve its priority too.

Include <mc/queue.h> and link your CMake target with Catalyst::Catalyst.

#include <mc/queue.h>

mc::CPriorityQueue<int> queue(1024);
if(!queue.push(42, 5.5)){
  // Keep the work and retry later, or handle unavailable queue storage.
}

int item;
double priority;
if(queue.pop(item, priority)){
  // item == 42 and priority == 5.5 in this example.
}

Higher numbers come first, including when priorities are negative. Equal priorities retain insertion order; overlapping pushes may be ordered either way. Positive and negative zero have equal priority. Positive and negative infinity are supported. NaN is rejected with CError, without inserting the item.

The queue copies simple, trivially copyable values, including pointers and ordinary scalar types. For a complex object such as cvar, use a pointer, for example CPriorityQueue<cvar*>. A pointer is borrowed: the queue neither deletes the object on pop nor deletes queued objects when the queue is destroyed. Keep pointed-to objects alive until their users finish. Null pointers can be queued, so use the Boolean return from pop to distinguish success from an empty queue.

The constructor allocates all queue storage. The default capacity is 1,024 entries; supply a positive capacity for your workload and inspect it with capacity(). Valid pushes, pops, and empty() allocate no memory. Always check the Boolean result of push: storage may be full or temporarily held by concurrent operations, even after an item has been removed. A failed push leaves ownership with the caller and does not insert anything.

pop returns immediately when no item is available and leaves its output arguments unchanged. Operations may retry under contention; lock-free does not guarantee a fixed completion time for each caller. empty() is a momentary observation, so call pop directly when retrieving work. New higher-priority work can arrive after a pop has selected its item.

Insertion cost grows linearly with queue length and, in the worst case, capacity. An uncontended pop takes constant time. This makes capacity and typical queue depth useful considerations when choosing this queue for a workload. Stop all users before destroying the queue. The queue cannot be copied or moved.

Containers and C++23

Catalyst containers use standard C++23 method names for standard operations, alongside Catalyst conveniences. Include the header named after the type, such as <mc/CVector.h>. Related methods and their overloads are grouped in each header: construction, assignment, iterators, capacity, element access, lookup, modifiers, container operations, and output.

CatalystStandard counterpart
CVector, CDeque, CList std::vector, std::deque, std::list
CMap, CMultimap, CSet std::map, std::multimap, std::set
CHashMap, CHashSet std::unordered_map, std::unordered_set
CFlatMap, CFlatSet std::flat_map, std::flat_set
CArray, CArrayBuf, cstr std::array, std::span, std::string

Use ranges

Construct owning sequences and associative containers with std::from_range. Sequences support assign_range, append_range, and insert_range; lists and deques also support prepend_range. Associative containers accept insert_range, and strings also provide replace_with_range. These operations consume the supplied range immediately. Views over an existing container retain their usual lifetime and iterator-invalidation requirements.

#include <array>
#include <ranges>
#include <mc/CVector.h>

std::array source{1, 2, 3, 4};
auto even = source | std::views::filter([](int n){ return n % 2 == 0; });
mc::CVector<int> values(std::from_range, even); // {2, 4}
values.append_range(std::array{6, 8});           // {2, 4, 6, 8}
values.insert_range(values.begin(), std::array{0});
mc::erase_if(values, [](int n){ return n > 4; }); // {0, 2, 4}

Use mc::erase, mc::erase_if, and mc::swap where the corresponding container supports them. Standard algorithms can work with compatible Catalyst iterators. An API that requires a particular standard container type, including qualified helpers such as std::get and std::getline, may require .std() explicitly. CFlatSet::std() returns a live std::flat_set reference. Use stdCopy() for an independent standard flat set; changes to that copy do not affect the original.

Existing Catalyst conventions

Methods such as has, popBack, and dump remain available. For example, popBack() returns the removed value, while standard pop_back() returns nothing. Numeric-index overloads such as CVector::erase(index) coexist with standard iterator overloads. CFlatSet::replace(oldKey, newKey) replaces one key, while replace(container) replaces the complete sorted, unique sequence. CFlatMap::keys() returns a reference; use keysCopy() for an independent copy.

The specialized CSVector, CPVector, and CVectorSet retain their own contracts. In particular, CVectorSet::erase(value) returns nothing, and a duplicate insert(value) returns {end(), false}. Standard sets return an erased count and an iterator to the existing element, respectively. These existing meanings have been preserved. The compatibility reference lists the remaining differences and name overlaps.

Container operations translate escaping failures into CError subclasses, including COutOfRangeError and CLengthError. Operations performed directly on iterators, elements, or a standard container obtained through .std() follow their own exception rules. CArray checks excess initializer-list elements at runtime, rather than rejecting them during compilation as std::array does.

Values and CSON

mc::cvar holds a Catalyst value. mc::cvec is a vector of values, and mc::cmap maps string keys to values. Maps are useful for named fields; vectors preserve order.

#include <mc/cvar.h>
#include <mc/CSONParser.h>

using namespace mc;

cvar user = cmap{{"name", "Ada"}, {"active", true}};
user["name"] = "Ada Lovelace";

CSONParser parser;
cvar settings = parser.parse(R"({theme: "dark" sizes: [12 16 24]})");

CSON accepts quoted strings, numbers, booleans, nulls, maps, vectors, sets, symbols, and expressions. Commas are optional. Bare value names become symbols, so write type: "integer" with quotes in a database description. Parsing an expression preserves it without evaluating it.

parse(code, config = false) accepts either a null-terminated const char* or a cstr. parseFile(path, config = false) reads a file. Each call must contain one complete value or expression. Results own their contents and remain valid after the input or parser is destroyed. You can reuse a parser after either a successful parse or an error.

CSON also accepts single-quoted strings, // and /* ... */ comments, and = between a map key and its value. A map head such as {widget, width: 20} stores "widget" under the key "#". none denotes the missing-value sentinel.

Configuration strings

Pass true as the second argument to expand $(NAME) from the process environment inside string values. This applies to nested maps, vectors, sets, and function arguments. Map keys and symbols remain literal. Expansion does not evaluate expressions or change the types of values.

CSONParser parser;
cvar settings = parser.parse(
  R"({cache: "$(HOME)/Library/Caches/my-app"})", true);
cvar fileSettings = parser.parseFile("settings.cson", true);

Adjacent references and variables with empty values are supported. A variable's replacement text is inserted once; references inside that text are not expanded again. An undefined variable retains its $(NAME) spelling and emits a warning. In a CSON string, write "\\$(NAME)" to suppress expansion; the parsed string retains the backslash. Ordinary parsing leaves all references literal.

Tokens and parsing errors

parser.tokens() returns classified source spans from the most recent parse, including names, numbers, strings, and operators. Each CToken has a type, byte index, and byte size in the original input. Long strings occupy adjacent tokens. Copy the token vector if you need to keep it across another parse; tokens do not retain the input text.

Syntax errors throw CParseError, with a one-based source line and, for files, the path. File access errors throw CError. Inputs are limited to 16,777,215 bytes by the token offset representation. Excessively nested input or value trees report a parse error. Use a separate parser for each concurrent call.

Use value.isMap(), isVec(), and other type checks before accessing an unknown value. Use value.at("name") for an existing map field and value.getVec().at(0) for a vector element. cNone is a distinct missing-value sentinel; a default-constructed cvar is null.

Native values and callable functions

cvar::wrap(value, kind) holds a C++ value inside a cvar. An lvalue is copied and an rvalue is moved. Retrieve it with unwrap<T>(description); a type mismatch throws CError. The description appears in the error message.

cvar name = cvar::wrap(cstr("Ada"), 900);
name.unwrap<cstr>("name") += " Lovelace";

auto resource = std::make_unique<int>(42);
cvar owned = cvar::wrap(std::move(resource), 901);

Copies of a wrapped value share the same object. They do not copy its contents again. The object is released when its final owning cvar is released. Synchronize access to shared mutable contents. The kind argument is application metadata; it does not replace the C++ type check.

Use Func0 through Func15 to store callable functions taking zero through fifteen const cvar& arguments and returning a cvar.

cvar increment = cvar::wrap(
  cvar::Func1([](const cvar& value) -> cvar{
    return value + 1;
  }), cvar::Lambda1);

cvar result = increment(41); // 42
cvar functions = cmap{{"increment", increment}};
result = functions["increment"](9); // 10

The supplied callable must use the matching FuncN type. An incorrect argument count, an empty function, or a callback failure reports CError. Lambda0 through Lambda15 identify these callable kinds. A callable executes when invoked; an ordinary symbolic cfunc remains an unevaluated expression.

Wrapped objects, callables, and iterators are process-local C++ values. They cannot be written with store(), pack(), or save(), including when nested in a collection. Convert their contents to ordinary values before saving or sending them. Python and Swift bindings continue to exchange portable value snapshots and reject these native objects.

Custom objects

Derive from CObject and implement execute(const cfunc&) to give an object behavior through a cvar. The function name selects the operation and its arguments provide the inputs. Check both the name and argument count.

class Counter : public CObject{
public:
  cvar execute(const cfunc& f) override{
    if(f.name == "Add" && f.args.size() == 1){
      return value_ + f[0];
    }
    if(f.name == "AddBy" && f.args.size() == 1){
      value_ += f[0];
      return cNone;
    }
    if(f.name == "Numeric" && f.args.empty()){
      return value_;
    }
    return fail(f);
  }
private:
  cvar value_ = 0;
};

cvar counter(new Counter);
counter += 3;
cvar next = counter + 2; // 5; counter still holds 3
double count = counter.as<double>();
OperationObject method
+ - * / %Add Sub Mul Div Mod
+= -= *= /= %=AddBy SubBy MulBy DivBy ModBy
ComparisonsEQ NE LT GT LE GE
Unary minus, logical negationNeg Not
Prefix/postfix increment and decrementInc PostInc Dec PostDec
as<bool>(), numeric conversionBool Numeric
String/container/expression conversionsString Vector Set Map Symbol Function
Numeric indexing, append with <<, dereferenceIdx Push Star
Collection operationsThe corresponding name, such as at, get, erase, pushFront, size, begin, or find

Use value.execute(cfunc("method", arguments...)) to invoke another method directly. Unsupported operations and exceptions from custom code report CError. size(), empty(), and span() can now throw when an object's method fails.

Operations returning a C++ reference, including at(), numeric indexing, front(), back(), and reference conversions, require the object to return a borrowed cvar reference to live storage, for example return &memberValue;. That storage must remain valid for the caller's use. Returning a temporary value is rejected.

Implement canExecute() to advertise optional behavior. A supported Str() supplies the display string. Objects defining equality must also supply Hash() to be used as set elements. Return an ordinary value representing their equality: equal objects must produce equal hash values, and values affecting equality must remain unchanged while stored in a set.

Iterators and erasure

begin_(), end_(), and find(value) return iterators held in cvar values. They support vectors, function arguments, sets, maps, and custom objects. Link your target to Catalyst::Shared to use these operations. The ordinary C++ begin() and end() remain available.

cvar values = cvec{10, 20, 30};
for(cvar it = values.begin_(), end = values.end_(); it != end; ++it){
  cvar value = it.Star();
  // Use value here.
}

values.erase(values.find(20));
values.erase(values.begin_(), values.end_());

cvar text = "abcdef";
text.erase(2, 2); // "abef": byte position and byte count

find() searches sequence values, set members, or map keys. A miss returns the end iterator. Star() returns a borrowed reference to a vector or function argument, a copy of a set member, or a map with key and value snapshots for a map entry. Set keys cannot be mutated through an iterator.

Copying an iterator cvar shares its position; it++ returns an independent iterator at the previous position. These are forward iterators. Incrementing or dereferencing an end iterator created by these methods throws CError.

Keep the container alive and follow its usual iterator invalidation rules. Structural edits, reallocation, rehashing, replacement, and erasure can invalidate iterators; obtain fresh ones afterward. Iterators do not extend the container's lifetime. An erase range is [first, last); both endpoints must belong to that container and occur in forward order. An empty range is allowed.

Python provides value.erase(position, count=1); Swift provides try value.erase(at: position, count: count). These erase vector/function elements or string bytes. Counts are clamped at the end, negative inputs are rejected, and existing snapshots remain unchanged. For UTF-8 text, choose byte boundaries that preserve whole characters.

Construct runtime objects

cvar::construct(name, arguments...) creates a registered object. Give the returned pointer to an owning cvar. Unknown class names and invalid constructor arguments throw CError. Link with the supplied core library to use these factories. The Microcosm interpreter initializes the built-in factories when it first constructs a native object or imports a module; no separate initialization call is required.

cvar queue(cvar::construct("Queue_cms"));
queue << 42;
cvar item = queue.execute(cfunc("pop"));

cvar random(cvar::construct("Random_cms", 123));
cvar sample = random.execute(cfunc("uniform"));
Class nameOperations
Random_cmsSeed management and random distributions; optional constructor seed.
Regex_cmsConstruct with a pattern; match(text) or match(text, &captures) with a vector for captures.
Queue_cmsPush, blocking pop, empty, disable, and react(callback) to consume currently queued values.
Pool_cmsstart(threadCount) and a borrowed pool pointer from ptr().
Messenger_cmsconnect, send, receive, onReceive, onClose, and close.
Data_cmsConstruct with a CSV path; inspect size, width, and data, normalize numeric columns, or drop(column).
Database_cmsOpen an existing database containing schema.cson. table(name) returns a table with lookup, insertion, update, erasure, traversal, query, and commit operations.

On macOS, link Catalyst::MacShared to construct Draw_cms, Plot_cms, and Movie_cms. Drawing and movie constructors take width and height; plotting takes a plot configuration. Drawing fonts use a name followed by a size, such as Helvetica 18. Drawing render(path) and plotting save(path) write JPEG images; movie render(path, fps) exports video. Plotting setData(name, values) copies its input.

A messenger's receive callback uses Func1 and returns a Boolean indicating whether it handled the message. Its close callback uses Func0. Passing null clears a callback. execute(cfunc("callbackError")) retrieves and clears a callback failure by throwing CError. A receive timeout returns null. A receive-callback failure closes that connection; construct a new messenger to reconnect. Finish calls before releasing their objects, and release captured resources when callbacks no longer need them.

Unix timestamps and local time

Include <mc/time.h> to construct, inspect, parse, and format timestamps on macOS and Linux. The Unix epoch is January 1, 1970 at 00:00:00 UTC. Calendar fields and formatted strings use your process's local time zone. To use UTC throughout a program, start it with TZ=UTC0 ./my_program.

#include <mc/time.h>

double seconds = mc::cEpochMake(2024, 7, 1, 12, 30, 45.25);
uint64_t nanos = mc::cMakeNano(2024, 7, 1, 12, 30, 45, 250000000);

int year, month, day, hour, minute, second;
mc::cEpochUnmakeSeconds(seconds, year, month, day, hour, minute, second);
mc::cEpochUnmakeNano(nanos, year, month, day, hour, minute, second);

mc::cstr text = mc::cNanoTimeStr(nanos); // "2024-07-01 12:30:45"
mc::cstr today = mc::cTimestamp("%Y-%m-%d");
uint64_t midnight = mc::cNanoTime("2024-07-01", "%Y-%m-%d");
FunctionUnits and behavior
cEpochMake(year, month, day, hour, min, seconds) Returns Unix seconds as double. The seconds field can include a fraction and must be in [0, 60). Dates before the Unix epoch produce negative values.
cMakeNano(year, month, day, hour, min, sec, nsec) Returns Unix nanoseconds as uint64_t. sec is 0–59; the optional nsec fraction is 0–999,999,999.
cEpochUnmakeSeconds(seconds, ...)
cEpochUnmakeNano(nanos, ...)
Fill year, month, day, hour, minute, and second outputs. An additional overload takes a weekday output between day and hour. Weekdays run from Sunday = 1 through Saturday = 7. Fractional seconds are rounded down to the containing second; for example, −0.5 seconds belongs to the last second of 1969 when viewed in UTC.
cTimeStr(seconds, format)
cNanoTimeStr(nanos, format)
Format nonnegative Unix seconds or nanoseconds, respectively. The default format is %Y-%m-%d %H:%M:%S. Nanosecond fractions are omitted.
cTimestamp(format) Format the current wall-clock time. Its default format is %Y-%m-%d %H:%M. Use a steady clock for measuring elapsed durations.
cNanoTime(text, format) Parse local calendar text into Unix nanoseconds. The default format is %Y-%m-%d %H:%M:%S. Custom formats must supply a complete calendar date; omitted clock fields are zero. Trailing whitespace is allowed, but trailing non-whitespace text is rejected.

Formatting and parsing use the standard put_time and get_time format syntax. The process's time zone determines calendar conversion; a format string does not select a different zone. Parsing does not accept fractional seconds. Use cMakeNano() when supplying a nanosecond fraction.

The original cEpochUnmake() overloads remain available: double arguments mean seconds and integral arguments mean nanoseconds. Integer literals are accepted, and negative integral arguments report CError. Prefer the explicit unit names in new code, especially when passing values between functions.

Invalid dates, invalid clock fields, nonfinite seconds, conversion failures, and values outside the destination's range report CError in every build configuration. Dates are not normalized: February 29 in a non-leap year is an error. Unix nanoseconds must lie between zero and UINT64_MAX, ending at 2554-07-21 23:34:33.709551615 UTC. Earlier or later timestamps are rejected rather than wrapped. Second-based conversions are also limited by the platform's local calendar support.

Daylight saving time is determined automatically. A nonexistent local time, such as 02:30 during a spring transition that skips that hour, is rejected. When an hour repeats in autumn, conversion uses the platform's choice of occurrence. Preserve the Unix timestamp when the particular occurrence matters; plain local calendar text cannot distinguish them. These functions can be called concurrently while the process's time zone and locale remain unchanged.

Dates across billions of years

cMakeTime() and cUnmakeTime() represent dates as exact integer seconds over a range large enough to describe time since the Big Bang. Their fixed epoch is January 1 at 00:00:00 in year CTimeEpochYear, which is −13,700,000,000. That instant has value zero. The epoch is a calendar convention and does not change with scientific estimates of the universe's age.

uint64_t beginning = mc::cMakeTime(mc::CTimeEpochYear); // 0
uint64_t first = mc::cMakeTime(2024, 12, 31);
uint64_t next = mc::cMakeTime(2025, 1, 1);
uint64_t elapsedSeconds = next - first; // 86400

int64_t year;
int month, day, hour, minute, second;
mc::cUnmakeTime(next, year, month, day, hour, minute, second);
// year = 2025, month = 1, day = 1; clock fields are zero.

The timeline applies Gregorian leap-year rules to every year, including dates before the Gregorian calendar was adopted. It includes astronomical year zero (1 BCE); year −1 is 2 BCE. Every day has exactly 86,400 seconds. These functions do not apply time zones, daylight saving time, or leap seconds, and they do not reconstruct historical calendars. The Unix-time helpers likewise do not accept a seconds field of 60.

All uint64_t timestamp values can be decoded and encoded again without losing a second. Dates before the fixed epoch or beyond the timestamp range report CError. Keep these values as integers: converting a present-day cosmic timestamp to double loses second-level precision. The unsigned range spans about 584 billion years in seconds; using nanoseconds would reduce that span to about 584 years. Compare timestamps before subtracting if their order is unknown, because unsigned subtraction cannot express negative durations.

Calendar-only helpers are independent of the local time zone: cGetDayInfo(year, month, day, dayOfYear, dayOfWeek) returns a one-based day of year and Sunday-based weekday. cDayOfYear(year, dayOfYear, month, day) performs the inverse ordinal-date lookup and takes an unsigned year. Both validate their input, including leap days. These helpers and the cosmic conversions can also be used in constant expressions.

Cosmic timestamps and Unix timestamps have different epochs. Do not pass a value from cMakeTime() to a Unix-time helper. Values saved using the earlier, incorrect cosmic conversion should be recomputed from their original dates: that conversion mapped some different dates to the same value, so recovery from a timestamp alone is not always possible.

Connect to a database

#include <mc/CDB.h>

mc::CDB db("dbname=catalyst_demo connect_timeout=5");

CDB accepts PostgreSQL connection strings, including connection URIs. Construction opens the connection and reports failures with a Catalyst exception. For a remote database, provide the host, database, user, and appropriate PostgreSQL authentication/TLS options. PostgreSQL password files and service definitions can keep credentials out of application source.

Each instance represents one connection. Calls on an instance must be serialized. Use separate instances or application-managed connections for concurrent work. Rows copied or returned by CDB own their values and can outlive the connection.

Describe and create tables

createTable() accepts a map-valued cvar, a cstr containing CSON, or a CSON string literal. Each describes one table:

db.createTable(R"({
  schema: "public"
  name: "users"
  columns: [
    {name: "id" type: "integer" nullable: false identity: true}
    {name: "accountId" type: "integer" nullable: false}
    {name: "name" type: "string" nullable: false}
    {name: "active" type: "bool" nullable: false default: true}
    {name: "settings" type: "map" default: {theme: "dark"}}
  ]
  primaryKey: ["id"]
  indices: [
    {name: "users_account_name" columns: ["accountId" "name"] unique: true}
  ]
})");
Table fieldMeaning
nameRequired table name.
schemaSQL namespace; defaults to public. The namespace must already exist.
columnsRequired nonempty vector of column descriptions, in column order.
primaryKeyOptional vector of native-column names. Supports composite keys; PostgreSQL enforces uniqueness and non-null values.
indicesOptional vector of index descriptions, created with the table.

Each column requires name and type. nullable defaults to true. An omitted default means no explicit default; default: null specifies a null default. Literal defaults use the column's declared type, including serialized collection values.

identity: true is available for integer columns. PostgreSQL generates a value when the field is omitted; explicitly supplied values are allowed. Identity columns cannot also specify a default. Sequence values may have gaps, including after rolled-back inserts.

For a SQL expression default, use defaultSql instead of default, for example defaultSql: "40 + 2". This field is SQL source, so supply an application-controlled expression.

Unknown fields, duplicate columns, unsupported types, and conflicting options produce errors. Existing tables are not overwritten. Creating a table and its indices is atomic, including inside a manual transaction.

Names in helper methods preserve case. A name can be schema-qualified, such as public.users. Quote a component containing a literal dot, for example "public"."sales.2026". Identifiers are limited to 63 UTF-8 bytes. Raw SQL follows PostgreSQL's quoting rules; the mixed-case accountId column needs double quotes there.

Choose column types

Description typeValueStorage
boolBooleanSQL boolean
integerSigned 64-bit integerSQL bigint
floatDouble, including infinities and NaNSQL double precision
stringUTF-8 string without NUL bytesSQL text
bufferBinary bufferRaw SQL bytea
symbol, function, vector, map, set, packed, noneThe corresponding Catalyst typeSerialized Catalyst value
cvarAny supported Catalyst valueSerialized Catalyst value
pointerA pointer addressSerialized address; not a persistent object

Use native columns for fields you filter, sort, join, aggregate, or use as keys. Use cvar when different rows need different value types, when a string contains NUL bytes, or when tag metadata must be preserved. Native columns reject metadata they cannot represent and report mismatched value types as errors.

Serialized values preserve nested collections, symbols, expressions, packed values, binary buffers, and type metadata through Catalyst's store() representation. Buffer contents are saved in full, independently of their current read position. Expressions are not evaluated. Untagged nulls become SQL NULL; cNone remains a separate serialized value. Nullability is enforced by the row helpers even for tagged nulls.

Buffer values must be contiguous bytes. Flatten structured buffers with attached child buffers before storing them.

References are saved as snapshots of their target values. Object values are unsupported because Catalyst does not define their serialization. Stored pointer addresses do not become valid objects in another process. Cyclic values are rejected; nesting is limited to 256 levels.

The contents of a serialized field are opaque to SQL. Comparing or indexing its bytes does not provide Catalyst value equality: maps and sets do not have a canonical byte ordering. Put searchable attributes in separate native columns. PostgreSQL fields are limited to approximately 1 GB, so large external assets should be stored separately.

Insert data

mc::cvar user = mc::cmap{
  {"accountId", 7},
  {"name", "Ada"},
  {"settings", mc::cmap{{"theme", "light"}}}
};

uint64_t inserted = db.insert("users", user);

A row is a map from column names to values. Missing columns use their defaults, generated identities, or SQL null where permitted. An empty map requests an all-default row. An explicit null writes SQL null. Unknown columns and incompatible values are errors. The return value is the number of inserted rows, not a generated identifier.

For bulk loading, pass a vector of row maps. All rows must have the same nonempty set of keys. Empty vectors insert zero rows. A batch succeeds completely or rolls back completely.

mc::cvec rows{
  mc::cvar(mc::cmap{{"accountId", 7}, {"name", "Grace"}}),
  mc::cvar(mc::cmap{{"accountId", 7}, {"name", "Margaret"}})
};
db.insert("users", mc::cvar(std::move(rows)));

Send large imports in bounded batches so the application does not need to build a vector containing the entire dataset.

Select and traverse rows

Use SQL to select the needed columns and filter the needed rows. Values are supplied separately through PostgreSQL's $1, $2, … parameter syntax.

db.queryMap(
  R"(SELECT id, name FROM users
     WHERE "accountId" = $1 AND id > $2
     ORDER BY id LIMIT $3)",
  mc::cvec{7, 0, 1000},
  [](const mc::cmap& row){
    std::cout << row.at("name") << '\n';
    return true;
  });

queryMap() supplies a map keyed by selected column names or aliases. Duplicate names are errors, including for empty results; use aliases when joining tables. query() supplies a vector in selected-column order and allows duplicate names.

These methods accept a single cursor-compatible SELECT or VALUES query, including ordinary SELECT CTEs. Data-changing statements and data-changing CTEs belong in execute(). SQL functions can still have their usual side effects.

Direct serialized columns, including aliases and joined table columns, are decoded to their Catalyst values. A SQL expression returning bytea, such as coalesce(settings, settings), produces an ordinary buffer because it no longer identifies a declared stored-value column. If that expression is known to contain serialized data, restore it explicitly:

mc::cvar row = db.get("SELECT coalesce(settings, settings) FROM users LIMIT 1");
if(!row.isNone() && !row.getVec().at(0).isNull()){
  mc::cvar settings(row.getVec().at(0).getBuf());
}

In raw SQL parameters, scalars use native representations, buffers use raw bytes, and collections or tagged values use serialized bytes. Provide a SQL cast when an expression has no inferred parameter type, such as SELECT $1::bigint or SELECT $1::bytea. Use the row helpers to store scalar values in a cvar column; they apply the declared column representation.

Other SQL result types, such as exact decimals and timestamps, return their PostgreSQL text representation. They are not silently rounded into Catalyst numeric values.

Get the first row

mc::cvar user = db.getMap(
  R"(SELECT id, name, settings FROM users
     WHERE "accountId" = $1 ORDER BY id)",
  mc::cvec{7});

if(user.isNone()){
  std::cout << "No matching user\n";
}
else{
  std::cout << user.at("name") << '\n';
}

get() returns a cvar containing a vector; getMap() returns one containing a map. Both return cNone when no row exists. A row whose fields are all null is still a row. The returned value owns its contents.

Only the first row is fetched. Extra rows are allowed; these methods do not check uniqueness. Use ORDER BY with a unique tie-breaker when you need a deterministic first row. A query's sort or aggregation may still require substantial server work before that first row exists.

Update and erase rows

db.update("users",
  mc::cvar(mc::cmap{{"active", false}}),
  mc::cvar(mc::cmap{{"id", 42}}));

db.erase("users", mc::cvar(mc::cmap{{"id", 42}}));

update(table, changes, match) changes the specified fields. Omitted fields remain unchanged. erase(table, match) deletes matching rows. Both return affected-row counts; zero is a successful operation with no matches.

match must be a nonempty map of native-column conditions. Conditions are combined with AND. Ordinary values use equality; null uses IS NULL. Updates require a nonempty changes map. Use parameterized SQL for more expressive conditions:

db.execute(
  "UPDATE users SET active = false WHERE id < $1",
  mc::cvec{100});

db.execute("DELETE FROM users WHERE active = $1", mc::cvec{false});

execute() accepts one SQL command without result columns and returns its affected-row count. Use it for DDL and general INSERT/UPDATE/DELETE commands. Use query()/get() for row results. DML with RETURNING is not supported by these traversal methods. Use CDB's transaction methods for transaction control and insert() for bulk loading.

Commit and roll back

Autocommit is enabled initially. Each successful operation commits independently. Related operations can share a transaction using either explicit control or a transaction callback.

Explicit transaction

db.begin();
try{
  db.insert("users", firstUser);
  db.insert("users", secondUser);
  db.commit();
}
catch(...){
  db.rollback();
  throw;
}

Manual-commit mode

db.setAutoCommit(false);
db.insert("users", firstUser);
db.insert("users", secondUser);
db.commit();

With autocommit disabled, the next database operation starts a transaction automatically. This includes reads and schema inspection. It remains active until commit() or rollback(). After either call, the next operation starts a new transaction.

Transaction callback

db.transaction([&](mc::CDB& tx){
  tx.insert("users", firstUser);
  tx.insert("users", secondUser);
});

Normal return commits; an exception rolls back. Transaction callbacks cannot be nested or take over transaction control with begin(), commit(), rollback(), or setAutoCommit().

Transactions use PostgreSQL's Read Committed isolation by default. A single query has a consistent query snapshot; successive statements can observe newly committed changes. Keep transactions short enough to avoid holding unnecessary locks and old row versions.

Create and remove indices

mc::CSONParser parser;
db.createIndex("users", parser.parse(R"({
  name: "users_active_id"
  columns: ["active" "id"]
})"));

db.dropIndex("users_active_id");

An index description requires name and an ordered, nonempty columns vector. unique defaults to false. method defaults to btree; hash, brin, gin, gist, and spgist can be requested where PostgreSQL supports them for the chosen columns.

For a populated table, concurrently: true requests an index build that allows ongoing writes. This requires autocommit and no active transaction, and cannot appear in a table's initial index list. A failed concurrent build may leave an invalid index; inspect it and drop it before retrying. Partial indices, expression indices, descending keys, and other advanced options can be created with explicit SQL.

A primary key supplies its own index. Additional indices cost storage and work on inserts and updates; choose them for actual query patterns.

Read table schemas

mc::cvar tables = db.schema();
mc::cvar users = db.schema("public.users");

// Recreate a supported definition in another database.
if(!users.isNone() && !users.getMap().has("unsupported")){
  destination.createTable(users);
}

schema() returns a vector of table descriptions, ordered by namespace and table name. It includes visible user tables across SQL namespaces and excludes PostgreSQL system tables. schema(table) returns one description, or cNone when the table is absent.

The descriptions use the same structure as createTable(). Column order, nullability, identity, supported defaults, primary keys, and indices are included. Defaults are normalized; formatting and CSON comments are not retained. Index descriptions are ordered by name and omit the implicit primary-key index. Construction options such as concurrently are not part of a stored schema.

Schema reads reflect current SQL definitions, including renames and changes made with SQL. A changed or externally defined default may be reported as defaultSql. A description with unsupported contains a list of features outside the current recreation vocabulary, such as foreign keys, custom SQL types, generated columns, partitioning, triggers, or advanced index options. CDB refuses to create a table from such a description. Use PostgreSQL schema tools when a complete migration of those features is needed.

Remove a table

db.dropTable("users");
db.dropTable("old_users", true);  // Missing table is acceptable.

This removes the table, its rows, and its own indices. The default is to report a missing table as an error. Pass true as the second argument to tolerate absence; dropIndex() offers the same option.

Dependencies from other objects prevent removal. If cascading removal is intended, issue explicit SQL. Normal table and index removal follows the current transaction mode and can be rolled back before commit.

Errors and connection lifetime

try{
  db.insert("users", user);
}
catch(const mc::CError& error){
  std::cerr << error.what() << '\n';
}

Validation errors, SQL errors, connection failures, and callback exceptions surface as CError or a subclass. SQL error messages include PostgreSQL's SQLSTATE. Translated external errors retain their original exception as a cause.

CDB does not reconnect or replay operations automatically. After a broken connection, construct a new instance and resolve any uncertain write outcome at the application level. After an ordinary SQL error in a manual transaction, roll back before continuing.

For a long traversal, some callbacks may already have run when a later fetch fails. A database rollback cannot undo unrelated work performed by those callbacks, such as writing a file or sending a message.

Work with large datasets

CDB can traverse results without accumulating the complete result set in application memory. A billion-row database still needs a schema, storage system, and maintenance plan matched to its workload.

A short client traversal does not bound server work for every query: sorts, joins, and aggregates can require substantial processing before rows are returned.

CDatabase tables and commits

mc::CDatabase stores indexed rows in a local directory. Construct it with a path and true to create a database, or false to open an existing one. Typed tables use CRow32 or CRow64; schema-defined tables return rows as cvec, with the row ID in element zero. Use the same row definition or schema when reopening a table.

Table insert, update, and erase stage changes until commit(). rollback() discards the pending batch. An update requires a live, committed row ID and gives the replacement a new ID. Read it again through an index or traversal before updating it in a later batch.

For repeated changes to one row in a batch, the last change wins. Updating a row does not increase the live row count. Erasing a missing or previously erased row has no effect. Unique indices reject duplicate live values, including duplicates within the pending batch; an erase can free a unique value for reuse in the same commit.

Queries and row-ID collections exclude erased and replaced rows. Traversal callbacks return true to continue and false to stop. Selected fields retain their original positions, with unselected fields returned as null. Collect changes during traversal and commit them after the callback returns. Numeric indices reject NaN values and NaN range endpoints.

Composite indices order by their first field within the group identified by the remaining key fields. Native values and equivalent cvar keys now use the same database hashing rules, including string literals and numeric keys of different widths. This changes composite index hashes from older releases; recreate existing databases when upgrading. Row definitions and Cosmic query syntax are unchanged.

commit() makes a table's pending batch visible in memory. To save periodically, call setSaveInterval(seconds) with a positive interval no greater than one billion seconds. Zero disables periodic saving; this is the default. Changing the interval replaces the previous schedule. Background save errors go to std::cerr, or the stream supplied to setErrorStream(stream).

After releasing table handles, call shutdown_() to finish saving and close the database. It reports save failures to the caller; after correcting the storage problem, call it again to retry. Repeated successful shutdown calls are harmless. Destruction also attempts to save, but reports failures through the error stream. Keep a supplied error stream alive until the database has been destroyed.

Only one database object or process may open a directory at a time. It may have multiple table handles. setMemoryLimit(megabytes) accepts a positive memory budget; active pages and pending batches can temporarily exceed it. Saved files are replaced after successful writes. A save across multiple files is not an atomic snapshot, and crash recovery for an interrupted database-wide save is not provided.

Keep the database alive while using its tables. Finish using table handles before erasing or renaming their table, or shutting down the database. Database and schema-table objects must not be copied.

Some original operations, including get and insert, still declare noexcept. Failures during those calls terminate the process. These declarations remain for interface compatibility.

Method reference

MethodResult or effect
createTable(definition)Create one table from cvar or CSON.
dropTable(table, ifExists = false)Remove a table and its data.
createIndex(table, definition)Create an index from a cvar description.
dropIndex(index, ifExists = false)Remove an index.
schema() / schema(table)All descriptions, or one description/cNone.
execute(sql, params = {})Affected-row count for one command without result rows.
query(sql, params, callback)Traverse vector rows; true on exhaustion.
queryMap(sql, params, callback)Traverse map rows; true on exhaustion.
get(sql, params = {})First vector row, or cNone.
getMap(sql, params = {})First map row, or cNone.
insert(table, rowOrRows)Inserted-row count.
update(table, changes, match)Updated-row count.
erase(table, match)Deleted-row count.
begin() / commit() / rollback()Explicit transaction control.
setAutoCommit(enabled)Choose automatic or manual commit while idle.
autoCommit() / inTransaction()Inspect transaction mode and state.
transaction(callback)Commit on return; roll back on exception.

Further PostgreSQL documentation: connections, SELECT, indices, partitioning, and vacuuming.

SQL over local tables

<mc/CSQL.h> provides SQL access to a local database directory. Create one with true, or reopen it with false. A borrowed CSQLDelegate receives each selected row during execute() and must outlive the database.

struct Results : mc::CSQLDelegate{
  void handle(mc::cvec& row) override{
    std::cout << row << '\n';
  }
} results;

mc::CSQL db(&results, "items.db", true);
db.execute("CREATE TABLE items (id INTEGER, name TEXT)");
db.execute("INSERT INTO items VALUES (?, ?)", mc::cvec{1, "first"});
db.execute("SELECT name FROM items WHERE id = ?", mc::cvec{1});

Use ? placeholders with a cvec of values. insert(sql, rows) accepts a vector of parameter vectors. insertCSV() and insertCSVFile() accept the same insert statement and a flag indicating whether to skip a header row. Mutating statements commit their changes; close the database normally to save it.

Numeric strings must contain a complete number. Values outside the declared integer width or the finite range of REAL are rejected before modifying rows. Table creation and deletion record enough information to recover their schema metadata after a failed save. Once the storage problem is corrected, the next operation or reopening reconciles the schema with the completed table operation. Check whether the table exists before retrying a failed change. CSQL objects cannot be copied.

By default results are vectors in SELECT-column order. After setMapped(true), implement handle(mc::cmap&) to receive rows keyed by column name. Copy a row to retain it beyond the callback. Boolean columns are represented as integers 0 and 1. CSQL uses the local SQL implementation; PostgreSQL connections and their transaction API use CDB.

Entries, attributes, and relations

<mc/CES.h> stores entries identified by nonzero IDs, named attributes, and directed relations. Reusing an attribute or relationship name returns its existing ID.

mc::CES store("entries.db", true);
auto earth = store.addEntry();
auto moon = store.addEntry();
auto mass = store.addAttribute("mass");
auto contains = store.addRelationship("contains");
store.setValue(mass, earth, 5.972e24);
store.addRelation(contains, earth, moon);
mc::cvar attributes = store.getEntryAttributes(earth);
auto moons = store.getRelationsFrom(contains, earth);
store.shutdown();

Values can be int64_t, double, or an arbitrary cvar. Use the corresponding typed getter and deletion method. Numeric attributes support equality and inclusive range queries. To replace a value, delete it and set the new value. An omitted relation weight is treated as zero.

deleteEntry() also removes its attribute values and incoming and outgoing relations. Deleting a named attribute or relationship removes the associated values or relations. Call compact() occasionally after substantial deletion. IDs are reserved on disk before being returned or used in new rows; an unsuccessful insertion can therefore leave a gap in the IDs. shutdown() saves and closes the database and may be called repeatedly. It reports save failures and can be retried after the storage problem is corrected. Other operations throw once shutdown has begun. Destruction reports save failures without throwing. If compaction fails, close and reopen the store before using it again. Reopen with CES(path, false); store objects cannot be copied.

Store frames and export movies

mc::CTheater stores movies as sequences of JPEG frames in a local directory. Each movie and each frame can carry cvar metadata. You can retrieve individual frames, reopen the collection later, or export a movie as H.264 video.

Include <mc/CTheater.h> and link Catalyst::Shared. Frame storage and retrieval need no external program. Video export requires ffmpeg with the libx264 encoder available to the command shell. On macOS, install it with brew install ffmpeg. FFmpeg chooses the output container from the filename; use .mp4 for an MP4 movie. See the FFmpeg documentation for its supported containers and encoding options.

Create a collection and add frames

#include <mc/CTheater.h>

int main(){
  mc::CTheater theater("frames", true);
  auto movie = theater.createMovie(
    640, 480, 85, mc::cmap{{"name", "Example"}});

  mc::CVector<unsigned char> pixels(640 * 480 * 4, 255);
  for(int frame = 0; frame < 24; ++frame){
    theater.addFrame(movie, pixels.data(), mc::cmap{{"frame", frame}});
  }

  theater.encode(movie, "movie.mp4", 24, 4000);
}

The constructor's create argument is true for a new directory and false to reopen an existing collection. The new directory must not already exist, and its parent must exist. An empty path is invalid. The class is noncopyable; only one open collection may own a given directory at a time.

createMovie(width, height, quality, metadata) returns a nonzero 32-bit movie ID. Dimensions must be positive and supported by JPEG. Quality ranges from 0 to 100 and defaults to 50; it controls JPEG frame storage quality. Metadata defaults to cnull. Movie information includes #id, #size (a vector containing width and height), and #quality; these reserved entries override values supplied in your metadata. movieInfo(id) returns that information, while getMovies() returns a vector of the information for all movies. Deleting a movie does not make its ID available for reuse.

addFrame(id, pixels, metadata) appends a frame, starting at frame index zero. On macOS and other little-endian systems, pixels are packed RGBA bytes in row order, with no row padding. Big-endian systems use ARGB. The input must contain at least width * height * 4 bytes; a raw pointer does not provide enough information to check its allocation size. The function finishes reading the buffer before returning, so you can then reuse it. JPEG compression is lossy and discards alpha; decoded frames are opaque. Frame metadata defaults to cnone and is stored independently from the movie metadata.

Read frames and maintain a collection

#include <cstdlib>
#include <memory>
#include <mc/CTheater.h>

int main(){
  mc::CTheater theater("frames", false);
  mc::cvar metadata;
  std::unique_ptr<unsigned char, decltype(&std::free)> pixels(
    theater.getFrame(1, 0, metadata), std::free);
  if(pixels){
    // Use the decoded pixels and this frame's metadata.
  }
}

The allocating getFrame(id, index, metadata) overload returns a buffer that you must release with free(). The overload getFrame(id, index, buffer, metadata) writes into your own buffer and returns true on success. Your buffer must have room for the movie's full decoded image. Both overloads restore the frame metadata. A missing movie or frame returns nullptr or false and leaves metadata and caller-supplied pixels unchanged. Invalid or corrupt stored images throw CError.

deleteMovie(id) removes the movie and its frames. Deleting an absent movie is a no-op. Call compact() after substantial deletion to reclaim database space. Changes follow CDatabase's commit and persistence behavior; allow the collection to finish destruction when closing it normally.

Export video and use multiple threads

encode(id, path, fps, bitrate) blocks until export finishes. Frame rate is a positive integer. Bitrate is in kilobits per second and defaults to 100000. H.264 export uses yuv420p, requires even width and height, and requires at least one frame. Export preserves the stored frames, so you can append more or encode again. It refuses to overwrite an existing output file. Invalid settings, unavailable encoders, and failed exports throw CError; a failed export can leave a partial output file.

The optional third constructor argument limits simultaneous encoders for that collection; it defaults to three and must be positive. Additional encoding calls wait for a slot. Frame insertion, retrieval, deletion, and compaction coordinate access across threads. Concurrent appends receive distinct consecutive indices, although their order depends on which call proceeds first. Export includes the frames present when its encoding slot starts; later appends are left for the next export. Deleting that movie while it is being encoded can fail the export. Keep the collection alive until every call using it has returned.

HTTP requests

<mc/CHTTP.h> provides synchronous requests with run(), or background requests with start() followed by await().

mc::CHTTP request(mc::CHTTP::JSON);
request.setURL("https://example.com/data.json");
request.setTimeout(10.0);
request.start();
mc::cvar response = request.await();
int status = request.status();

Select Text for a string, JSON for parsed values, or Buffer for binary data. Returned strings and buffers preserve embedded zero bytes and remain valid after the request object is reused or destroyed. Transport errors and invalid JSON throw; background errors are reported by await(). Check status() for HTTP error status codes.

setPost() accepts text or a cvar encoded as JSON. Use addHeader("Content-Type: application/json") when sending JSON. addPost() and postFile() build multipart forms. setType() selects a request method such as PUT; escape() URL-encodes a value. Configuration changes and escape() throw between start() and await(), even if the transfer has already finished. After await returns or reports a transfer error, the request can be configured and reused. status() may be polled while a request runs; it returns zero until a response code is available. Serialize calls to the other methods. Request objects cannot be copied.

Serve files with CHTTPServer

<mc/CHTTPServer.h> provides an HTTP server for a directory of files, with optional directory listings and executable pages. Link with Catalyst::Shared. The configuration is a cvar map, which can also be loaded from CSON. Supply an existing root directory and a port from 1 to 65535.

#include <iostream>
#include <mc/CProgram.h>
#include <mc/CHTTPServer.h>

int main(int argc, char** argv){
  mc::CProgram program(argc, argv);
  mc::cvar options = mc::cmap{
    {"root", "web"},
    {"port", 8500},
    {"host", "127.0.0.1"},
    {"allowHidden", false},
    {"runExecutables", false}
  };
  mc::CHTTPServer server(options);
  server.start();
  std::cout << "Serving http://127.0.0.1:8500/; press Enter to stop.\n";
  std::cin.get();
  server.shutdown();
}

Create web/index.html before running this example. A relative root is resolved against the working directory at construction. start() starts background request handling and returns; keep the application running while it serves requests. Invalid configuration and startup failures, including a port already in use, throw CError. Configuration is copied at construction; changing the original map does not reconfigure the server.

start() and shutdown() may be called repeatedly, including from different threads while the object remains alive. Calling start() on a running server does nothing; calling it after shutdown starts listening again. Shutdown stops accepting connections and releases the listening port. Requests already accepted may continue until completion or their configured timeouts; shutdown does not wait for them. Destruction also shuts down the listener. The server cannot be copied or moved.

Configuration

Option names are case-sensitive. Boolean options require boolean values, sizes and counts require integers, and timeouts accept integer or floating-point seconds. Timeouts must be finite and between 0.1 and 3600 seconds. Strings must not contain embedded zero bytes.

OptionDefaultMeaning
rootRequired Existing directory containing the served files.
portRequired TCP port, from 1 to 65535.
host"0.0.0.0" Address to listen on. The default accepts connections on all IPv4 interfaces. Use "127.0.0.1" for local access or an IPv6 address such as "::1".
index ["index.html", "index.htm"] A filename or vector of filenames to try in order for a directory request. Entries must be filenames, without directory separators; at least one usable entry is required.
directoryListingfalse Generate a linked directory listing when no index file is found. Otherwise a directory without an index returns 403.
allowHiddentrue Allow path components beginning with a dot. Setting this to false also excludes hidden index files and symlinks whose resolved targets have hidden components.
followSymlinksfalse Allow symbolic links only when their targets remain inside the root directory.
runExecutablestrue Run executable files without a filename extension as pages. Set false for a server that only serves file contents.
autoStartfalse Start listening during construction.
backlog128 Requested pending-connection backlog, from 1 to 4096.
maxConnections1024 Maximum active requests, from 1 to 1,000,000. Additional connections receive 503 while this limit is reached.
maxRequestBytes64 KiB Maximum request-line and header bytes, from 1 KiB to 16 MiB. An oversized header block receives 431.
sendBufferBytes64 KiB File-transfer buffer size, from 1 KiB to 16 MiB.
readTimeoutSecs5.0 Total time allowed to receive a complete request header block. An incomplete request that expires receives 408.
writeTimeoutSecs5.0 Timeout for a blocked socket write; this is not a total duration limit for a file transfer.
programTimeoutSecs10.0 Time allowed for an executable page to finish and close its output, including output inherited by child processes.
maxProgramOutputBytes1 MiB Maximum executable-page output, from 1 KiB to 128 MiB.
cacheSeconds0 Static-file cache lifetime. Zero sends Cache-Control: no-cache; a positive integer sends public, max-age=N. Static files also include Last-Modified.
cors, corsOrigin false, "*" Enable CORS response headers and set their allowed origin. Allowed methods are GET, HEAD, and OPTIONS.
serverName "catalyst-http-server" Value of the Server response header.
logfalse Write request summaries to standard error.

Requests and files

The server accepts HTTP/1.0 and HTTP/1.1 requests. GET returns file contents, HEAD returns the corresponding headers without a body, and OPTIONS returns the supported methods. Other methods receive 405. Each connection serves one request and then closes. File contents are binary-safe; the filename extension determines the content type. Directory requests without a trailing slash redirect to the slash form while preserving the query string.

URL paths are percent-decoded before lookup. Parent traversal, backslashes, malformed escapes, embedded zero bytes, and disallowed hidden or symbolic-link paths are rejected. Directory listings escape filenames for HTML and URLs and omit inaccessible symbolic links. HTTP/1.1 requests require a Host header; malformed headers are rejected. The current server does not provide TLS, authentication, custom request callbacks, request-body processing, chunked request decoding, range responses, or conditional cache validation.

Executable pages

With runExecutables: true, an executable regular file without an extension is run when requested, including when selected as a directory index. Other regular files are served as stored bytes. Each execution receives no command-line arguments. Its working directory is the directory containing the program, standard input is empty, and standard error is discarded. A successful exit returns standard output as an HTML response with caching disabled. Output is the response body; do not print CGI headers. A HEAD request also runs the program to determine its response headers but omits the body.

The program receives REQUEST_METHOD, REQUEST_URI, SCRIPT_NAME, SCRIPT_FILENAME, QUERY_STRING, DOCUMENT_ROOT, SERVER_PROTOCOL, SERVER_SOFTWARE, and SERVER_PORT environment variables. HTTP_HOST, HTTP_USER_AGENT, and HTTP_ACCEPT are supplied when those request headers exist. The query string retains its URL encoding; the program is responsible for interpreting it. The environment uses PATH=/usr/bin:/bin:/usr/sbin:/sbin and does not copy the application's environment.

Timeouts return 504. A failed execution, nonzero exit, or excessive output returns 500. Timed-out programs and their process groups are terminated. Programs run with the application's permissions; use trusted programs and keep their files and directories under trusted control while the server runs. The file-serving root does not restrict what a program itself can access.

Exchange messages with CWebSocket

<mc/CWebSocket.h> provides a secure WebSocket client with cvar messages. Link with Catalyst::Shared. Supply a hostname, an absolute request path, and an optional port to connect(). The default port is 443. Pass the hostname alone, without wss:// or a port suffix; query parameters belong in the path.

#include <iostream>
#include <mc/CWebSocket.h>

mc::CWebSocket socket;
if(!socket.connect("example.com", "/events")){
  throw mc::CError("could not connect to the WebSocket service");
}
socket.send(mc::cmap{{"subscribe", "updates"}});

mc::cvar message;
if(socket.receive(message, 5.0)){
  std::cout << message << '\n';
}

Replace the hostname and path with those of your WebSocket service. Connections always use TLS, including when an explicit port is supplied; plaintext ws:// connections are not supported. CHTTPServer serves HTTP files and pages and does not itself provide a WebSocket endpoint.

Connection and certificate checks

connect() returns true after the TCP connection, TLS handshake, and WebSocket upgrade succeed. It returns false for an invalid endpoint, a failed connection or handshake, or an attempt to connect an already connected object. A failed attempt can be retried. After a peer disconnects, the same object can connect again; starting a new connection discards unread messages and errors from the previous connection. There is no automatic reconnection.

The client requires TLS 1.2 or newer, verifies the certificate chain and hostname, and supplies the hostname through TLS SNI. It uses OpenSSL's default trust paths. For a private service, install its CA in that trust store, or set SSL_CERT_FILE to a PEM file containing the trusted CA before connecting. The certificate still needs to match the hostname or IP address passed to connect(). OpenSSL's trust store is separate from the macOS Keychain.

SSL_CERT_FILE=/path/to/private-ca.pem ./my_program

Connection establishment and each active write have five-second deadlines. Destruction attempts a normal WebSocket close, allowing one second for the exchange before closing the transport. Resolver cleanup and application callbacks must still finish. The object cannot be copied or moved. Destroy it from outside its delegate callbacks, after other application threads have finished using it.

Sending and receiving values

send(value) writes one JSON text message. Ordinary JSON values—null, booleans, numbers, strings, vectors, and maps—are the simplest interchange format. Strings must contain valid UTF-8; embedded zero characters are encoded as JSON escapes. Symbols, functions, and non-finite numbers use the representations provided by CJSONGenerator, which require corresponding interpretation by the receiving application. Sets, buffers, packed values, pointers, and objects cannot be sent directly; convert them to suitable JSON values first. Excessively nested values are rejected.

Incoming payloads are parsed as CSON, so JSON is accepted alongside CSON syntax. WebSocket fragments are combined before parsing, and ping/pong control frames are handled automatically. A received message may contain at most 16 MiB. Protocol violations or oversized messages close the connection. Payload parsing failures and exceptions from a delegate are reported by receive() as CError; they do not terminate the process.

OperationBehavior
send(value) Waits for the message to be written and throws CError on failure or when disconnected. Concurrent callers' writes are serialized. Inside a delegate callback, sending queues the message and returns without waiting; the callback must return before transmission can complete.
receive(message) Waits for an unconsumed message or disconnection. Returns true and assigns the message, or false when disconnected with no queued messages remaining.
receive(message, seconds) Also returns false when its timeout expires. Zero polls without waiting. The timeout must be finite, nonnegative, and representable by the system clock.

Queued messages remain available after disconnection until read or discarded by a new connect() attempt. A false receive result leaves the supplied value unchanged. Sending and receiving may run concurrently; if multiple threads receive, each queued message is delivered to one of them.

Handle messages with a delegate

Install a CWebSocketDelegate when some messages should be handled as they arrive. Its handle(socket, message) method returns true to consume that message, or false to place it in the receive queue. Changes made through the mutable message reference are retained when it is queued.

class Updates : public mc::CWebSocketDelegate{
public:
  bool handle(mc::CWebSocket* socket, mc::cvar& message) override{
    if(message.isMap() && message.has("heartbeat")){
      socket->send(mc::cmap{{"heartbeat", "acknowledged"}});
      return true;
    }
    return false;
  }
};

Updates updates;
mc::CWebSocket socket;
socket.setDelegate(&updates);

The delegate is borrowed. Keep it alive until it has been replaced or the socket has been destroyed. setDelegate(nullptr) restores the default behavior of queueing all messages, and delegate() returns the currently installed delegate. Replacing it from another thread waits for an active callback to finish. Callbacks for one socket run sequentially on a background thread. Keep them short; they must not block waiting for the socket's own incoming messages. A blocking receive() inside a callback throws CError; a zero-timeout poll is allowed.

Native messaging and server delegates

<mc/CMessenger.h> and <mc/CServer.h> provide asynchronous messaging and server connections. Link with Catalyst::Shared. Start a CPool for I/O, then pass that pool to your server and messengers.

Implement CServerDelegate::admit(server, messenger, auth) to inspect a connection's first message. Returning false rejects and deletes the pending messenger; returning true transfers ownership to your application and queues {"#":"authenticated"} for the client. Install a CMessengerDelegate and initialize the messenger's session during admission. Keep accepted messengers alive until admission has finished; do not delete one from admit().

CMessengerDelegate::handle(messenger, message) runs when a message arrives. Return true to consume it, or false to leave it for receive(). Native messaging preserves serializable cvar data, including symbols and expressions. Use send() to queue a reply; didClose() reports peer closure or errors, while an explicit local close() is silent.

The server borrows its delegate and pool. Admission callbacks may run concurrently; protect shared state. Destroy the server before its delegate so pending connections and active admission callbacks finish. The application still owns accepted messengers: destroy them while their delegates remain alive, then stop the pool. The runnable Cosmic example in examples/28-server/main.cc demonstrates this order, rejected and accepted admissions, echo messages, and closure. listen(port) binds all IPv4 interfaces; the example's client connects over loopback.

JPEG and image buffers

Include <mc/image.h> and link Catalyst::Shared. cCreateJPEG(pixels, width, height, quality, bytes) encodes four-byte pixels and returns the encoded size through bytes. Quality ranges from 0 to 100. The native pixel order is RGBA on little-endian systems and ARGB on big-endian systems; cCreateJPEG_BGRA() explicitly accepts BGRA instead.

cReadJPEG(jpeg, bytes, width, height) decodes into the native pixel order and reports the dimensions. Release decoded storage with delete[], for example through std::unique_ptr<unsigned char[]>. Release encoded storage from either JPEG encoder with std::free. JPEG is lossy and does not preserve alpha.

cAddImage(dst, src, dstWidth, dstHeight, x, y, width, height) copies a rectangle into a destination buffer. It copies pixels without alpha blending and rejects placement outside the destination. cAddJPEG(dst, jpeg, dstWidth, dstHeight, bytes, x, y) decodes and places a JPEG. Supply sufficient storage for each image's dimensions times four bytes per pixel.

Read CSV data

#include <mc/data.h>

mc::CVector<mc::cstr> columns;
mc::cvec rows = mc::cFromCSV("name,count\nEarth,1\nMoon,2", columns);

The first row supplies column names. Each following row becomes a vector; complete numeric fields become numbers, and other fields remain strings. Quoted commas, doubled quotes, multiline fields, CRLF, and a final row without a newline are accepted. Rows with the wrong number of fields and malformed quoting throw a parsing error.

Link the Mac graphics library

The macOS SDK includes the graphics library libcatalyst_mac. On macOS, mc-cosmic automatically links it together with the core libcatalyst library when producing an executable. With CATALYST_SDK set to the extracted SDK directory as shown in SDK setup:

"$CATALYST_SDK/bin/mc-cosmic" main.cc -o my_graphics_app

Use --link FILE to add other object files or libraries; repeat it for each additional input. Object-only compilation with -c does not link libraries.

In CMake, link with Catalyst::MacShared to use it together with the core library, or select Catalyst::MacFramework for framework integration. Your application uses the Catalyst headers; the CMake targets provide the bundled dependencies.

find_package(Catalyst CONFIG REQUIRED)
add_executable(my_graphics_app main.cpp)
target_link_libraries(my_graphics_app PRIVATE Catalyst::MacShared)

For Cosmic .cc sources, call catalyst_cosmic_directory("${CMAKE_CURRENT_SOURCE_DIR}") after defining your targets. The drawing and rendering classes require an available Metal device. Keep these resource-owning objects in one place; do not copy them by value.

Draw and render images

#include <mc/CDraw.h>

mc::CDraw canvas(640, 480);
canvas.setFillColor(mc::double4{1.0, 1.0, 1.0, 1.0});
canvas.clear();
canvas.setFillColor(mc::double4{0.2, 0.4, 0.9, 1.0});
canvas.fillRect(mc::double2{20.0, 20.0}, mc::double2{100.0, 60.0});
canvas.render();
unsigned char* rgba = canvas.buffer();

CDraw supports paths, shapes, text, clipping, transforms, hit testing, and images. Drawing is completed by render(). buffer() returns width × height × 4 RGBA bytes. You may supply a buffer to the constructor or resize(); keep it large enough and alive until the canvas stops using it. An internally allocated buffer remains owned by the canvas and may change on resize.

Shapes, text, and images are composited in the order submitted. drawImage(data, srcTopLeft, srcSize, dstTopLeft, dstSize) reads tightly packed RGBA bytes. srcSize is the full source image width and height; srcTopLeft selects the first source pixel, cropping through the bottom-right edge. Use {0, 0} to draw the whole image. Source dimensions and positions must be whole pixels within the image. The selected pixels are scaled to the destination size and copied during the call, so the source buffer can be modified or released before rendering. Reusing a source buffer submits its new pixels.

Canvas dimensions must be positive and within the device's Metal texture limits. If resizing throws, the old buffer, dimensions, and pending drawing remain available. A successful resize discards pending drawing and clears a newly allocated buffer. Passing the canvas's own buffer back to resize retains its ownership and requires dimensions that fit the existing image.

<mc/CRender.h> provides 3D rendering. Set a camera and projection, create box or sphere models or load a GLB file, then call clear(), draw(), and render(). Read the RGBA result through buffer() and release models with unload(). The library carries its drawing shaders; no source-tree shader path is required at runtime.

Plot data

<mc/CPlot.h> takes a CSON-style description of dimensions, data columns, and layers. Use symbols for column mappings; strings represent literal values.

using namespace mc;
cvar config = cmap{
  {"width", 640}, {"height", 480},
  {"data", cmap{{"x", cvec{0, 1, 2}}, {"y", cvec{0, 2, 1}}}},
  {"layers", cvec{cmap{{"geom", "line"},
    {"aes", cmap{{"x", csym("x")}, {"y", csym("y")}}}}}}
};
CPlot plot(config);
plot.plot();
unsigned char* rgba = plot.buffer();
plot.setData("y", CPlot::Floats{2.0, 0.0, 1.0});
plot.plot();

The buffer contains the configured width × height × 4 RGBA bytes. It is owned by the plot, and is unavailable before the first plot(). setData() invalidates the previous image; call plot() again and obtain a fresh buffer pointer.

Run Metal compute functions

<mc/CGL.h> provides CGLRun for a compiled .metallib and a named kernel function.

mc::CGLRun run("kernels.metallib", "scale");
mc::CVector<float> values{1.0f, 2.0f, 3.0f};
run << values;
run.setGrid(values.size());
run.run();

Arguments occupy Metal buffer slots in the order added. The kernel must match their element types and layout. setGrid() specifies thread counts, and setThreadGroup() optionally selects the thread-group dimensions. Invalid or overflowing dimensions throw before dispatch.

Use start() and await() for asynchronous work. Input bytes are read at start and results are written back at await. Keep argument storage alive and at the same address through await; do not resize a bound vector. Repeated runs read the current input values. clear() removes argument bindings between runs.

After user arguments, the runtime supplies scratch storage, its total byte count, log storage, its total byte count, and the total thread count. These count buffers hold 64-bit unsigned values. setMemory() and setLog() specify bytes per thread. After await, outputLog(thread) writes that thread's bounded log text. GPU command failures throw from await.

Files, paths, and system helpers

Include <mc/system.h> and link Catalyst::Shared for these helpers on macOS and Linux. Existing inline helpers remain usable with Catalyst::Catalyst. All existing function names are retained.

#include <mc/system.h>

auto path = mc::cJoinPath(mc::cUserHome(), "logs", "app.log");
mc::cCreateDirs(mc::cParentDir(path));
mc::cAppendToFile("started\n", path);

auto temporary = mc::cCreateTempDir();
auto settings = mc::cJoinPath(temporary.path(), "settings.txt");
mc::cSaveAtomic("enabled=true\n", settings);
for(const auto& line : mc::cReadLines(settings)){
  std::cout << line << '\n';
}
PathsBehavior
cJoinPath(a, b, ...)Join any number of components. Empty components are skipped. An absolute component replaces the preceding path. An initializer list is also accepted.
cNormalizePath(path)Remove redundant separators and lexical ./.. components without accessing the filesystem.
cAbsolutePath(path, base = "")Resolve a relative path against base (the current directory by default), then normalize it lexically. The path need not exist.
cRelativePath(path, base = "")Express a path relative to base, using lexical absolute paths. Both relative arguments are interpreted from the current directory.
cCanonicalPath(path)Resolve symlinks and return an absolute path. Every component must exist.
cExpandPath(path)Expand environment variables and a leading ~ or ~/. Named-user forms such as ~someone are rejected. No globbing or shell commands run.

Lexical normalization can change the meaning of paths containing a symlink followed by ... Use cCanonicalPath when resolution through the actual filesystem is required.

File contents and operationsBehavior
cStrToFile(text, path), cAppendToFile(text, path)Create or replace text, or append to it. Embedded NUL bytes are retained. Writes report incomplete I/O as CError.
cFileToBuffer(path)Read a whole file into an owning CBuffer, with its cursor at the beginning.
cReadLines(path), cForEachLine(path, visit)Return CVector<cstr> or stream one line at a time. Remove LF and its preceding CR; preserve empty lines, embedded NULs, and a final unterminated line. A trailing LF does not add an extra empty line. A visitor returning false stops reading.
cSaveAtomic(text, path), cSaveAtomic(buffer, path)Write and sync a temporary file in the same directory, then rename it over the destination. Readers see the complete old or new file. A final symlink is replaced, leaving its target alone. Existing regular-file rwx permissions are retained; new files use 0600. Ownership, ACLs, extended attributes, and special permission bits are not copied. The parent must exist. This guarantees atomic visibility on a filesystem supporting atomic rename, not survival of a power loss.
cCreateDirs(path)Create missing parents; return whether a directory was created. An existing directory is accepted. The existing cCreateDir keeps its stricter behavior.
cCopyFile(source, destination, overwrite = false) Copy a regular file; a source symlink is followed. An existing destination requires overwrite; a destination symlink is rejected. Parents must already exist.
cCopyTree(source, destination, overwrite = false) Copy directory contents, including hidden entries, into destination. Merge existing directories and preserve links without following them. Reject special files, symlinks where a destination directory is needed, and copies into the source tree. Overwrite permits replacing leaf entries, including symlinks themselves. New directories use normal creation permissions. A failed copy can leave partial results.
cRemove(path), cRemoveTree(path) Remove one entry or recursively remove a directory tree. Symlinks are removed without traversing their targets. Return a boolean or the number of removed entries; missing paths return false or zero. Nonempty directories require cRemoveTree.

cFileInfo(path, followSymlinks = false) returns a CFileInfo with type, size in bytes, writeTime in Unix seconds, and POSIX permissions. Types are CFileType::Missing, File, Directory, Symlink, and Other. A dangling link remains a Symlink unless following is requested. cIsSymlink(path), cReadLink(path), and cCreateSymlink(target, path) inspect or create links; relative targets are interpreted from the link's directory.

mc::CWalkOptions options{.recursive = true, .hidden = false,
                         .followSymlinks = false};
auto sources = mc::cFindFiles("src", "*.cc", options);
mc::cWalkDir("src", [](const mc::cstr& path, const mc::CFileInfo& info){
  std::cout << path << '\n';
  return true; // false stops the entire walk
}, options);

cFindFiles matches basenames using *, ?, and [...], returning sorted regular-file paths prefixed by the supplied root. By default both functions recurse, skip names beginning with a dot, and do not follow symlinks. Following includes links to regular files and traverses each physical directory at most once, preventing cycles. Walk callbacks receive link metadata and all entry kinds; they do not receive the root itself. Walk order is unspecified. Permission, traversal, and callback failures propagate as CError; entries are not silently skipped on errors.

cUserHome() uses an absolute HOME value or the OS user record. cHome() continues to mean the framework's MC_HOME. cTempDir() uses the OS temporary directory, and cExecutablePath() locates the running executable. cConfigDir(application = ""), cCacheDir(application = ""), and cDataDir(application = "") return locations without creating them. On macOS, config and data use ~/Library/Application Support; cache uses ~/Library/Caches. Linux uses absolute XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_DATA_HOME, falling back to ~/.config, ~/.cache, and ~/.local/share. Application names must be single path components.

cCreateTempFile(dir = "", prefix = "catalyst-") and cCreateTempDir(dir = "", prefix = "catalyst-") reserve unique resources with 0600 and 0700 permissions. Empty dir selects cTempDir(). Their move-only CTempFile and CTempDir owners expose an absolute path() and clean up at scope exit, including a temporary directory's contents. remove() performs checked cleanup; release() returns the path and transfers cleanup to you. Destructors make a best-effort cleanup without throwing. The old cTempPath still generates a name without reserving it.

cHasEnv(name) distinguishes an empty variable from a missing one. cEnv(name, fallback) uses its fallback only when the variable is missing; cUnsetEnv(name) removes it. cExpandEnvs(text, strict = true) expands $NAME, ${NAME}, and $(NAME) once, returning a new string. A backslash escapes a dollar sign. Missing variables throw; strict=false leaves their expressions intact. The existing cReplaceEnvs retains its in-place behavior and syntax. Coordinate environment changes with other threads using the environment.

cMonotonicNow() returns steady-clock seconds; cElapsed(start) measures elapsed seconds from such a value. These are suitable for measuring work, not dates. cNow and cTicks retain their existing behavior.

cHostName() returns the full OS hostname, retaining punctuation and domain components. The old cHost retains its abbreviated form. cDiskSpace(path = ".") returns CDiskSpace{capacity, free, available} in bytes, with available reflecting space available to an unprivileged user. cProcessMemory() returns CProcessMemory{resident, virtualBytes} for this process. cAvailableMemory() estimates available RAM in bytes: free plus inactive pages on macOS, MemAvailable on Linux. Memory and disk snapshots may change immediately after the call.

cFileToStr and cSaveBinary now check complete I/O. CSBuffer checks mapping failures and closes the file descriptor after mapping. Its copies still alias the same mapping; destruction does not release it. Call its new close() exactly once per mapping, after every alias is finished, and before reusing an owning instance for another mapping.

Run commands and capture output

CCommand runs shell commands on macOS and Linux. It is header-only and is available through <mc/CCommand.h>. Link your CMake target with Catalyst::Catalyst.

#include <mc/CCommand.h>

mc::CCommand command("printf 'Hello from Catalyst\\n'",
                     mc::CCommand::Output | mc::CCommand::Error);
int result = command.await();
mc::cstr output, error;
command.readOutput(output);
command.readError(error);

The constructor starts the command immediately. By default it uses /bin/bash -lc, which reads login-shell startup files. Add Zsh to use /bin/zsh -ic, which reads interactive-shell startup files. Install the selected shell at that path; zsh is optional and may need installing on Linux. Startup files can affect the command's environment and captured output. Commands use shell syntax, including quoting, pipes, expansion, and redirection.

Zsh terminal job control is disabled for commands with redirected or nonterminal input. Interactive startup files still load in this mode.

For a direct argv launch, use CCommand::fromArgs({"git", "status", "--short"}, mode). It searches PATH and passes each argument unchanged; spaces and shell metacharacters are ordinary argument bytes. The Zsh mode is not accepted by this factory. command() returns the executable name supplied as argv[0].

For a complete command result in one call, include <mc/system.h> and link a compiled Catalyst library:

auto result = mc::cRun({"git", "status", "--short"});
std::cout << result.out;
std::cerr << result.err;
if(result.status != 0){ /* command failed */ }

auto copied = mc::cRun({"/bin/cat"}, "input bytes\n");
auto pipeline = mc::cRunShell("printf hello | tr a-z A-Z");

cRun(arguments, input = "") captures both streams while sending input followed by EOF. It waits for completion and returns CRunResult{status, out, err}, retaining embedded NUL bytes. Nonzero exit status is returned, while launch or I/O failures throw CError. Signal termination uses the same status encoding as CCommand below. cRunShell(command, input = "") explicitly runs /bin/sh -c without login or interactive startup files. cFindExecutable(name) searches PATH and returns an absolute executable path, or an empty string if none is found. These convenience calls capture the entire output in memory and have no timeout; use CCommand for incremental output and lifecycle control.

ModeBehavior
InputEnable write() and closeInput() for the command's standard input.
OutputCapture standard output.
ErrorCapture standard error separately.
OutputWithErrorCapture both streams in one shared stream. Read it through either output or error methods; reading consumes those bytes for both. Cannot be combined with Output or Error.
PersistentAllow the command to keep running after its object is destroyed or resource-manager shutdown occurs.
ZshSelect the interactive zsh shell.

Combine modes with |. Streams that are not redirected inherit the application's corresponding standard streams.

mc::CCommand command("exec /bin/cat",
                     mc::CCommand::Input | mc::CCommand::Output);
command.write("first line\nsecond line\n");
command.closeInput();             // Send EOF; safe to call again.
mc::cstr output;
command.readOutput(output);       // Append all output through EOF.
int result = command.await();

write() waits until the entire string has been written or reports CError. Strings may contain binary data, including NUL bytes. A closed input pipe reports an error without terminating the application with SIGPIPE. A failed write may already have delivered a prefix; do not assume it can safely be retried in full.

The one-argument readOutput(text) and readError(text) append bytes through EOF. Their (text, timeout) overloads append currently available bytes, waiting up to the given number of seconds for data or EOF. They return false on timeout and true on data or EOF; true with no appended bytes means EOF. A zero timeout polls. Timeouts must be finite, nonnegative, and representable.

matchOutput(regex, matches, timeout) and matchError(regex, matches, timeout) attempt a full CRegex match against the unread stream, appending the whole match and capture groups to matches. For a suffix match, include a prefix such as [\s\S]* in the pattern. A successful match consumes the matched bytes. Timeout or EOF without a match returns false and leaves the bytes available for another match or read. Each call uses one total waiting budget; new data does not restart it. Regular-expression evaluation itself is not interruptible.

Both captured streams continue to drain during reads, writes, and await(), so filling one pipe does not prevent reading the other. Captured bytes stay in memory until consumed. Read incrementally for large or continuous output. Methods on a live command can be called from different threads; reads of the same stream serialize. Closing input, closing the command, or calling await() cancels a concurrent unfinished write.

Command status and lifetime

await() closes standard input, waits for the immediate child to exit and captured streams to reach EOF, and returns the exit status. Output remains available afterward. Descendants that inherit captured streams can delay EOF; use close() to stop waiting for their output. An ordinary nonzero shell exit is a result, not an exception.

StatusMeaning
-1status() has not yet observed child exit.
0 through 255Normal shell exit code.
ErrorStatus - signalNumberThe immediate child terminated because of that signal.
NoStatusExit status is unavailable, for example because another part of the application reaped it.
ErrorStatusUnexpected termination status.

status() polls without waiting. Completed results are cached, so repeated status() and await() calls agree. processId() returns the owned child PID, or -1 after it has been reaped. Let CCommand own child waiting: do not call waitpid() for its PID or install a competing global child reaper. It requires waitable children and rejects SIGCHLD settings that automatically discard status.

signal(number) signals the immediate child and is harmless after completion. close(true) closes redirected streams, signals the command with SIGTERM, and waits for cleanup. setCloseSignal(number) changes that first signal. Cleanup escalates to SIGKILL after one second, or when the group leader exits. close(false) starts the same cleanup and returns immediately; the child is still reaped. Both close forms are safe to repeat. Processes that leave the group, or outlive an already-reaped leader, are outside this ownership.

Commands with piped input, or whose inherited input is not a terminal, have their own process group; closing them also signals that group. Commands that inherit terminal input stay in the terminal's group so they can read it normally. In that case, closing targets the immediate child. Select Input when you want to supply input yourself and include subprocesses in group cleanup.

A command cannot be copied or moved. Destroying a normal command performs the equivalent of close(true). CResourceManager::shutdown() also closes normal commands and can be called repeatedly. A Persistent command is skipped by that shutdown; destroying it sends EOF on redirected input and discards subsequent captured output while allowing it to finish. Explicit close() still stops a persistent command.

Finish application calls on an object before destroying it. System failures and invalid arguments report CError; destruction completes cleanup without propagating command errors.

Run commands across machines with Nexus

mc-nexus runs shell commands concurrently on SSH destinations selected from named groups. It supports command aliases, multiple instances per machine, collective logging, and cancellation of the current batch. It runs on macOS and Linux and reads commands either interactively, with line editing and session history, or from standard input.

Start Nexus

Nexus and its helper are included in the SDK's bin directory. After configuring your environment, customize conf/mc-nexus.ccfg for your own machines and commands, then launch:

mc-nexus

The SDK also provides bin/nexus as a relative symbolic link to mc-nexus, so you can launch the command as nexus.

Configure SSH keys or an agent and verify the destination host keys before using Nexus. Connections use SSH batch mode: password and host-key confirmation prompts are unavailable. Remote machines need Bash 3.2 or newer. Nexus supplies the helper when it connects, so remote machines do not need Catalyst, mc-run, or mc-nexus-run installed. Commands run through a Bash login shell, including its startup files. They have no interactive terminal and receive EOF on standard input.

Configuration and commands

The supplied configuration is conf/mc-nexus.ccfg. Nexus uses CProgram's configuration order: command-line options, mc-nexus.ccfg in the current directory, directories in CM_CONF_PATH, then $MC_HOME/conf/mc-nexus.ccfg. A minimal configuration is:

{
  directory: "~/project"
  group: "workers"
  groups: {workers: [worker1 worker2]}
  logging: true
  logPath: "$(HOME)/log/nexus"
  commands: {
    build: {command: "cmake --build build --config Release"}
    worker: {command: "./worker", instanced: true, threaded: true}
  }
  instances: {worker1: 2, worker2: 1}
  threads: {worker1: 4, worker2: 8}
}

Use quoted strings for top-level string settings such as group and directory. Machine names inside groups can be strings or CSON symbols. A destination may also be an SSH alias or user@host. Every group must contain distinct destinations. A nonempty machine setting selects a single destination and overrides group.

mc-nexus -group workers -logging -verbose

# At the Nexus prompt:
build
worker -port 5585!
machine = "worker1"
instances = {worker1: 3}
verbose = true
cd "build directory"
pwd
reconfigure
quit

An unrecognized command is sent as shell text to each selected machine, once per machine. An alias replaces its first word with the configured command and appends the remaining arguments. Aliases can override directory, group, and machine. Within alias command text, $host is replaced by the local Nexus machine's short hostname.

An alias with instanced: true uses the top-level instances map; otherwise it uses its own instances map, defaulting to one instance per machine. Counts must be nonnegative integers; zero skips that machine. Similarly, threaded: true selects the top-level threads map, while an alias's own threads map applies otherwise. A positive thread count appends -threads N to the command. An alias's args map appends additional shell text for each destination. Raw shell commands do not use these alias instance or thread settings.

reserve, defaulting to two, reduces an instance count only when the destination exactly matches the local short hostname and its requested count equals the local hardware thread count. It subtracts the reserve only when the count is larger than the reserve.

Assignments to existing settings take CSON values, validate them, and apply to subsequent commands. An invalid assignment leaves the previous configuration in use. cd and change update the remote working directory; cd alone selects the remote home directory. Relative paths are appended to the selected directory, and ~ or ~/path refers to each remote user's home. A failed directory change prevents the command from executing there. reconfigure reloads configuration while retaining the current remote directory. exit, quit, or EOF leaves Nexus. historyCount sets the in-memory history limit.

Output and logging

verbose: true displays output as it arrives. An unquoted, unescaped trailing ! enables this for one command. Output lines identify the destination, zero-based instance, and stream, for example worker1[0] out: ready. Without verbose output, failures still show a bounded tail of stderr. silent: true suppresses successful completion messages.

logging: true creates a fresh run-... directory below logPath for every batch. Each instance has separate host.instance.out and host.instance.err files containing its raw output. Special characters in destination names are encoded in filenames. collective.log contains labelled output and completion summaries from all instances. Its order reflects when Nexus observes output; it does not establish an exact event order across machines or between stdout and stderr.

Logs are written incrementally, including final output without a newline. Very long lines are split into chunks in the collective log and console; raw stream files retain the original bytes. Log files are opened before commands start, and write failures cancel the batch. Verbose output alone does not enable logging or overwrite earlier logs.

Interrupts and batch input

Ctrl-C cancels the current batch and returns to the interactive prompt. At an idle prompt it discards the unfinished command. Cancellation first sends SIGINT to each job, waits interruptGrace seconds (default two), then sends SIGTERM if needed and waits the same interval before forcing cleanup. Cancellation targets that job and its process group. Commands that deliberately detach into a different process group, such as daemons, are outside that cleanup.

SIGTERM, SIGHUP, and SIGQUIT cancel active work and exit Nexus. Loss of the SSH input connection also requests remote cancellation. If the connection stalls or cleanup cannot be confirmed, Nexus reports remote cleanup unconfirmed and stops waiting after a bounded interval. connectTimeout, default ten seconds, limits SSH connection establishment. Remote cleanup cannot be guaranteed during a network partition.

printf 'build!\n' | mc-nexus -group workers

Commands read from a pipe run one batch at a time. Exit status is zero when the last operation succeeds and one when it fails. Interrupting a batch from piped input exits with 130; termination by another handled signal exits with 128 plus its signal number. Use one command per invocation when its exit status must describe that command alone.

Build and run Cosmic programs

The mc-cosmic command compiles a Cosmic .cc file into an executable, an object file, or LLVM IR. Cosmic combines C++ types and expressions with conveniences such as indentation-based blocks, % for return, string interpolation, properties, and generated method forwarding. See Cosmic language features for the syntax and behavior of these extensions.

The SDK includes mc-cosmic. With the environment from SDK setup, compile and run a program directly. Keep Xcode or the Command Line Tools selected with xcode-select so the compiler can find Apple's SDK and linker:

mc-cosmic -g -o hello hello.cc
./hello

A complete minimal hello.cc is:

#include <mc/CProgram.h>
using namespace mc;

int main(int argc, char** argv)
  CProgram program(argc, argv)
  cvar answer = 42
  print answer
  %0
# Produce an object to link with other C++ objects or libraries.
mc-cosmic -c -g -o hello.o hello.cc

# Optimize an executable.
mc-cosmic -O2 -o hello hello.cc

# Write LLVM IR.
mc-cosmic --emit-llvm -o hello.ll hello.cc

For programs that use the Mac graphics library, see Link the Mac graphics library. mc-cosmic links it automatically on macOS when producing an executable.

For integration with other build tools, combine -c with --depfile hello.d to write header dependencies. Repeat --clang-arg ARG to pass C++ compiler flags, for example --clang-arg -fPIC for a shared-library object. The SDK's CMake integration supplies the relevant target options automatically.

The compiler accepts one input file per invocation. Without -o, output uses the input's base name in the current directory, with the appropriate extension for object, IR, or Metal output. Failed compilation preserves an existing output file. Repeat -I DIR to add include directories and -D NAME=VALUE to define macros used by Cosmic source and ordinary C++ headers. Such headers can provide classes, functions, and templates used from Cosmic.

Quoted and angle-bracket includes, such as #include "cx/Plot.hpp" and #include <Metal/Metal.hpp>, contain ordinary C++. These headers retain ordinary C++ syntax and formatting when used from Cosmic. This also applies to .h and .hxx headers. No special directive is needed for .hpp.

A C++ #include takes effect where it appears in the Cosmic source. The included code can use preceding declarations and definitions in its enclosing scope, along with the current macro definitions. Fragments may introduce namespace declarations, class members, or statements inside a function. Indent directives inside an indented function body just like its other statements.

Use #define and #undef to configure subsequent source and includes. Macro changes made by headers remain available to later source. Header guards and #pragma once retain their C++ behavior; an unguarded header can be included repeatedly with different macro values:

#define VALUE_TYPE u1
#define INDEX_TYPE u1
#define ROW_TYPE u4
#include "table_index.h"
#undef ROW_TYPE
#define ROW_TYPE u8
#include "table_index.h"
#undef ROW_TYPE
#undef VALUE_TYPE
#undef INDEX_TYPE

Diagnostics and debug builds retain the original Cosmic and included C++ filenames and line numbers. Dependency files include the fragments used during compilation.

Cosmic preprocesses source before parsing declarations, statements, and expressions. This supports object and function macros, stringification, token pasting, variadic macros with __VA_OPT__, and macro include names. Use #if, #ifdef, #ifndef, #elif, #elifdef, and #elifndef to select source; inactive branches are not parsed. #error, #warning, #line, and _Pragma have their C++23 behavior. Diagnostics and debug information honor logical filenames and lines set by #line.

Expanded source still follows Cosmic's spacing and newline rules. Backslash continuations splice physical lines before those rules apply. Macros from ordinary C++ headers retain C++ operator, lambda, and literal meanings. Tokens written in Cosmic source, including macro arguments, retain Cosmic conventions. Unknown pragmas are ignored. Supported pragmas include header guards with once, macro stacks with push_macro and pop_macro, and declaration packing with pack.

Modules and header units

Named modules support interface and implementation units, partitions, export declarations, and global and private module fragments. Imports retain C++ visibility rules. A named module does not export its macros; a header unit makes its macros available after its import.

// example.cc
export module example;
export int answer()
  return 42

// consumer.cc
import example;
int main()
  return answer() == 42 ? 0 : 1

Compile importable units before their consumers. Write the interface with --precompile, compile its object with -c, and supply its object when linking:

mc-cosmic --precompile example.cc -o example.pcm
mc-cosmic -c example.cc -o example.o
mc-cosmic --clang-arg -fprebuilt-module-path=. \
  --link example.o consumer.cc -o consumer

For explicit interface paths, repeat --clang-arg -fmodule-file=example=example.pcm instead. A partition named example:part uses example-part.pcm with a prebuilt-module directory. Repeat --link FILE for other objects or libraries. --depfile FILE works with -c and --precompile and records imported interfaces as well as source and header dependencies.

Use --header-unit source.hh -o source.pcm to build a Cosmic header unit. Supply each imported header interface using --clang-arg -fmodule-file=source.pcm, then write import "source.hh"; or an angle-bracket import. Only supply the interfaces needed by that source. Precompiled interfaces require compatible compiler versions, targets, and options; rebuild them when changing the compiler. A standard library module such as std requires its own compatible interface.

A textual include that overlaps an eagerly loaded header unit may be rejected before that unit is imported. Import the header unit before such includes.

Cosmic's implicit Catalyst headers belong to the global module. Module source retains Cosmic operators, strings, and lambda conventions.

Cosmic language features

Cosmic keeps C++'s type system, object lifetimes, overload resolution, and library interoperability, and adds concise notation for common Catalyst operations. The extensions below are available when compiling a .cc file with mc-cosmic; they are not syntax accepted by an ordinary C++ compiler. Examples use the mc namespace. Include the headers for the APIs you use; #include core is convenient for examples using Catalyst values and collections.

This section describes the current compiler. Features with native support have examples you can compile into objects or executables. Additional syntax retained by the language is listed separately under current limits.

Runnable language examples

The SDK includes small programs that demonstrate these forms with checks that remain active in Release builds. Start with lesson 00, then choose a topic. Each path below is relative to examples/; the executable is named example- followed by its lesson name.

SourceLanguage features to try
00-cosmic-syntax/main.ccPrimitive and container types, aliases/constants, inferred values/references, generics, const methods/references, concise returns, and .now().
05-strings/main.ccRegex escapes with single backslashes, captures, and ordinary string operations.
12-threads/main.ccScoped guards around shared state in worker callbacks.
14-database/main.ccDirect named row initializers, generated fields, and indexed queries.
30-cosmic-casts/main.ccAll four casts, null pointers, moves, power versus XOR, and positional access.
31-cosmic-messages/main.ccOptional commas, embedded CSON, fields/defaults, symbolic expressions, trailing map arguments, and dispatch.
32-cosmic-control/main.ccCounted and infinite loops, short control statements, trailing lambdas, guards, errors, output, logging, and assertions.

After configuring the tutorial project:

cmake --build examples-build --target example-cosmic-messages
./examples-build/example-cosmic-messages

Layout, punctuation, and includes

An indented body begins on the line after its declaration or control statement. Use spaces, with a consistent increase in indentation; returning to the enclosing indentation ends the body. A newline normally ends a statement. Put the first statement immediately after the signature or control statement; an empty first body line can confuse the current parser's indentation detection. Braces and semicolons are also accepted, so you can use explicit blocks when that makes the scope clearer. Local variables are destroyed at scope exit, including when leaving through a return or exception, just as in C++.

int absolute(int value)
  if value < 0
    %-value
  else
    %value

int absoluteWithBraces(int value){
  if(value < 0){ return -value; }
  return value;
}

Commas are optional between unambiguous initializer elements and call arguments. This also applies inside embedded CSON. Whitespace separates the arguments below; it does not create a new scope or change their types:

int add(int x, int y) x + y

[i4] values = {1 2 3}
int total = add(20 22)
cvar settings = {name: "Ada" retries: 3}

Retain a comma when expressions would otherwise combine: add(20, -2) has two arguments, while add(20 - 2) has one subtraction expression. Commas in template argument lists and other C++ grammar are separate from this convenience.

Adjacent string literals still concatenate. Use {"ready", "local"} for two string elements; {"ready" "local"} supplies the single string "readylocal".

Function, class, struct, and control-flow bodies can put their opening brace on the following line, including after blank lines. The brace can be indented without adding another scope. An indented body that starts with a nested brace block and then continues at that body's indentation keeps its existing nested scope.

int absoluteWithBraces(int value)
{
  if(value < 0)
  {
    return -value;
  }
  return value;
}

Spaces and tabs are allowed before call parentheses, including sizeof (int), decltype (value), and catch (int caught). They are also allowed around ., ->, and ::, around template brackets, between a lambda's capture list and parameters, and in constructor, destructor, and operator names. For example, item . get (), Box <int >, [] (int n){ return n; }, ~ Box (), and operator + (int n) are accepted.

Within explicit brace bodies, member access can continue on the next line, such as item followed by . get (). Indented bodies still end a completed expression at a newline, so a following shorthand call such as .sleep(0.1) remains a separate statement. Template delimiters and lambda capture/parameter boundaries may also span lines. When a fully spaced expression could already mean chained comparisons, that interpretation is preserved: a < b > c remains a comparison. Keep Box<T> compact in an ambiguous expression.

Parentheses around if, while, and switch conditions are optional. Put each switch or dispatch case's body on following indented lines, even inside a braced switch. public:, protected:, and private: retain their C++ meanings. A named end can close a class or namespace in the style used by the framework:

namespace demo{

struct Item{
  int value = 0
end Item

end namespace demo

Spacing is part of the syntax. Put spaces around ordinary binary operators, especially *, /, %, <, ?, and |. Adjacent punctuation is used for type modifiers, templates, field access, and other shorthands. For example, left / right is division, function/value is a call, value < limit is a comparison, and Box<int> names a template specialization. Use parentheses to make a compound expression's grouping explicit.

An unquoted include lists Catalyst header names without their directory or extension. The following includes <mc/core.h>, <mc/CBuffer.h>, and <mc/thread.h>:

#include core CBuffer thread

Use quoted or angle-bracket includes for ordinary C++ headers, including generated .hxx files and third-party .hpp files. Their contents use C++ syntax. Definitions preceding the include are visible inside it. See the compiler section for macro behavior and include paths.

Types, aliases, and inferred declarations

Cosmic accepts ordinary C++ types and provides short primitive names. The number in most numeric names is the storage width in bytes on the supported targets. These names denote native scalar types, so arithmetic, conversions, overflow rules, and overload selection follow C++. They do not turn the value into a cvar.

SpellingMeaning
b1bool.
c1, uc1char and unsigned char.
i1/i2/i4/i8Signed 1-, 2-, 4-, and 8-byte integers; i8 is int64_t.
u1/u2/u4/u8Unsigned equivalents; u8 is uint64_t.
s8size_t, an unsigned size/index type. The s does not mean signed.
f2/f4/f8Half precision _Float16, float, and double.

In a type, T# means const T& and T% means const T*. The latter makes the pointed-to value const, not the pointer itself. Normal pointer and reference modifiers remain available. These shorthands also apply to container and template types, such as [int]# and Box<int>#. Reference and pointer declarations borrow their targets; they do not acquire ownership.

Container type shorthands select Catalyst containers: [T] is CVector<T>, {T} is CHashSet<T>, and {K: V} is CHashMap<K, V>. They can be nested. They describe a type, so use normal initialization syntax to supply its contents.

Nested vectors may use [[int]] values or [ [int] ] values at namespace, class, or block scope. Give a function an explicit return type when prefix attributes could be confused with a nested vector return type.

Ints => [int]
Counts => {cstr: int}

void example(cstr# name)
  Ints values = {1, 2, 3}
  Counts counts = {{"ready", 3}, {"waiting", 1}}
  cstr# label = name
  c1% text = "read only"
Values => [f4]                 // using Values = CVector<f4>
f8 Pi => 3.141592653589793     // static constexpr double

[f8] values = {1.0 2.0 3.0}  // CVector<f8>
{cstr} flags = {"ready", "local"} // CHashSet<cstr>
{cstr:u8} countMap = {{"ready" 2}} // CHashMap<cstr, u8>
{cstr:cvar} settings = {{"enabled" true}}
// With a declared class Foo, [Foo] is CVector<Foo>.

Name => Type declares a type alias, equivalent to using Name = Type. Adding a type before the name changes the meaning: i4 Limit => 128 declares a static constexpr value. Its initializer must be a constant expression; inside a class it is a static member, not a separate value stored in every object.

name := expression infers a variable's type. name :=> expression infers a reference to an existing lvalue. Assigning through that reference changes the original value. Its target must outlive every use of the reference. An inferred declaration from an unsuffixed decimal integer uses long; one from a plain string literal uses cstr. Use an explicit type when a particular width or overload is required.

count := 42
name := "example"
values := CVector<int>{1, 2, 3}
alias :=> values
alias[0] = 9
// values[0] is now 9.
x := 0
[f8] values = {1.0 2.0}
copy := values[0]
v :=> values[0]
v = 9.0
// copy is still 1.0; values[0] is now 9.0.

The element reference above is the equivalent of auto& v = values[0]. Keep the vector alive and avoid operations that invalidate its element references while using v. := follows Cosmic's literal typing; write i4 x = 0 when a four-byte integer is required.

Functions, concise returns, and constructors

%expression returns a value, and %void returns from a void function. The C++ spellings return expression and return also work. Returning from a lambda exits that lambda, even when the lambda is written as an indented or trailing block inside another function.

A typed function or method with a single expression immediately after its signature returns that expression. An indented or braced body uses explicit returns. Omitting the return type declares a void function; it does not infer a return type. Append # to a method signature to make it const.

C++ coroutines support co_await, co_yield, and co_return in functions, templates, lambdas, and methods. Their return type supplies the C++ promise interface; include <coroutine> for standard handles and awaiters. Custom operator co_await and promise await_transform operations participate in suspension. Coroutine returns follow Cosmic's usual newline termination rule.

int twice(int value) value * 2

struct Counter{
  int value_ = 0

  int value()# value_
  setValue(int value) value_ = value

  reset()
    value_ = 0
    %void
}

The const method above can read the object through a const reference; its # does not make the returned int a reference. Specify the return type separately, for example cstr# name()# name_, when returning a borrowed value.

struct Foo{
  f4 size_ = 2.0f
  f4 size()# size_
}

void showLabel(cstr# s){ print s; }

generic<T>
T twice(T value) value * 2

int foo(int x)
  if x > 2 %x * 2
  %x

generic<T> abbreviates template<class T>. The two uses of # above are independent: cstr# borrows a const string, while size()# makes the receiver const. A conditional directly on a function's signature line, such as int foo(int x) if x > 2 %x * 2, is parsed but currently rejected by native compilation. Put control statements in an indented or braced body, as above, and return on every required path.

Constructors may use a multiline initializer list with member = expression. The colon and subsequent entries align with the constructor, while its body is indented. These entries initialize members before the body runs. Initialization still follows member declaration order, regardless of the order of the entries.

struct Point{
  f8 x_
  f8 y_

  Point(f8 x, f8 y)
  : x_ = x
  y_ = y
    assert x_ >= 0
}

C++ initializer lists such as Point() : x_(0), y_(0){} are also accepted, including empty x_() and x_{} initializers. Constructors, destructors, inheritance, and explicit calls such as Base::read() retain their C++ behavior.

Generics

generic<T> introduces a type parameter without spelling class or typename. template<class T> remains available. Both forms can introduce class, function, member-function, and alias templates; they use C++ instantiation and overload rules.

generic<T>
struct Box{
  T value
  T get()# value
}

generic<T>
T twice(T value) value + value

generic<T>
Values => [T]

Typed parameters such as generic<b1 Enabled> are non-type parameters. Defaults, class partial specializations, explicit class and function specializations, and out-of-class template methods are supported. Parameter packs can expand in template argument lists. Template constructors retain their member and base initializers; new Box<int>(42) and braced construction use the usual constructor and aggregate rules.

Use a requires clause after the template parameter list or function signature to constrain a declaration, or use a concept and write, for example, generic<std::integral T>. Dependent types such as typename T::value_type and dependent member calls are resolved at instantiation. See current limits for template forms still awaiting support.

Define concepts with template<class T> concept Name = expression;. Requires expressions support simple expressions, typename T::type;, compound requirements such as { value.get() } noexcept -> std::same_as<int>;, and nested constraints such as requires sizeof(T) > 1;. Their operands are unevaluated. Substitution failures make a dependent requirement false; invalid nondependent requirements are errors.

Requires clauses support logical combinations such as requires Integral<T> && (sizeof(T) > 1). Put non-primary expressions in parentheses. Constraints participate in overload selection, class partial specialization, constructors, destructors, conversion functions, and generic lambda calls. Alias templates can also have a requires clause.

Constrained type parameters support qualified concept names, concept arguments, defaults, and packs, including template<std::same_as<int>... T>. Functions and lambdas accept constrained abbreviated parameters such as std::integral auto value and const std::integral auto& value. These constraints participate in deduction and overload selection.

Variables and function or lambda return types also accept constrained placeholders, including std::integral auto value = 42; and std::same_as<int&> decltype(auto) value = (source);. Non-type template parameters support simple constrained auto, as in template<std::integral auto N>. The current compiler does not support constrained non-type reference or pointer declarators, or constrained decltype(auto) non-type parameters; these constructs produce a diagnostic.

Variable templates support defaults, packs, requires clauses, and partial or explicit specialization. For example, template<class T> inline constexpr T answer = T(42); declares a separate value for each type. Use an explicit template<> head for a specialization. Array variables and static member templates use the same initialization and storage rules as ordinary variables.

Class templates can deduce their arguments from an initializer, as in std::pair values{20, 22};. Deduction also works with alias templates, aggregate initialization, construction expressions, new, and class-type non-type template parameters. A variable that uses a bare class template name cannot add a pointer, reference, or array declarator.

Declare a deduction guide in the same scope as its class template, for example template<class T> Box(T) -> Box<T>;. Guides support constraints and conditional explicit. A guide for a nested class template must have the same access as that template. Guides have no function body.

Define static data outside its class with a qualified name, such as int Counter::value = 42;. Class-template members use their template head, as in template<class T> T Counter<T>::value = T(42);. The initializer can use the class's private members. Write static on the declaration inside the class; retain thread_local on a thread-local definition.

Strings, interpolation, and literal suffixes

Names may use Unicode letters, such as π or 合計. Source files use UTF-8. Identifier characters follow the C++23 XID rules and must already be in Unicode normalization form C. The equivalent spellings \u03C0, \u{3c0}, and \N{GREEK SMALL LETTER PI} name the same identifier. Character properties and names use Unicode 17.0. Reserved C++ keywords cannot be used as names.

C++ digraphs are accepted: <% and %> mean braces, <: and :> mean brackets, and %: means the preprocessing hash sign. They follow the same whitespace rules as their ordinary spellings.

Double-quoted text is a string literal. In an explicitly typed declaration or argument it follows the destination's normal conversion rules; in text := "hello" it creates a cstr. Single quotes denote a character, not another spelling of a string. Adjacent string literals concatenate.

${expression} inside a string evaluates the expression and inserts its cStr() representation. Each occurrence evaluates once whenever the surrounding expression is evaluated. This is string construction, not a deferred formatting operation. Compute values beforehand if their relative side effects matter. Quotes used inside an interpolation in a double-quoted string must be escaped for the surrounding literal.

Interpolated strings can join adjacent ordinary and raw literals, for example R"(value: )" "${value}" R"( ${literal})". Raw fragments and C++ header-macro string fragments retain their literal contents. Null bytes in the constructed text are preserved. Interpolation produces a cstr; wide or UTF-prefixed fragments and literal suffixes cannot be combined with it.

cvar result = {answer: 42}
cstr message = "answer ${result.at(\"answer\")}"
cstr joined = "first " "second"
cstr lines = """first line
second line"""

Triple quotes permit literal newlines and quotes within the text. Indentation and the newline immediately after an opening triple quote are part of the value; Cosmic does not automatically dedent it. Interpolation also works in multiline strings. Ordinary escapes such as \n, \r, \t, escaped quotes, and Unicode \uXXXX sequences are supported. C++23 named and delimited escapes also work with interpolation and triple quotes, including \N{LATIN CAPITAL LETTER A}, \u{3a9}, \U0001f642, \x{41}, and \o{101}. Octal byte escapes consume up to three digits: \0 is a null byte, and \101 is A. In character literals, '\0' has numeric value zero. Use all three octal digits before a following digit: "\0007" contains a null byte followed by 7. A string literal still has its terminating null byte for C++ array operations: sizeof("add") is 4.

0p denotes nullptr. The numeric suffix n denotes an unsigned long literal, as in 0n; f denotes a float literal. Normal integer suffixes such as L, LL, UL, and ULL are also accepted. Do not use an integer suffix on a floating-point literal.

A leading zero denotes octal, so -0123 is -83. A decimal point or exponent instead denotes a decimal floating-point literal: 012.5 is 12.5. Binary and hexadecimal prefixes accept either letter case. The v numeric suffix is retained syntax without native support; use cvar(42) explicitly.

A string followed immediately by # computes its Catalyst hash: "open"# means cHash("open"). A number before the hash sign supplies the second hash argument: "scale"2# means cHash("scale", 2), useful for a function name with two arguments. These are hash values, not collision-free identifiers or string comparisons.

Unicode text hashes identically whether it is written as a C++ string literal or read at runtime. Symbol and function names retain their UTF-8 spelling when serialized or exchanged through Python and Swift bindings.

Regular-expression backslashes

Ordinary Cosmic strings preserve regex escapes such as \b, \s, \w, and \d, including their backslash. Write one backslash where an ordinary C++ string needs two. Constructing the string does not execute a regex; pass it to CRegex or another regex API.

#include core

CRegex words("\b[A-Za-z]+\s+[A-Za-z]+\b")
bool matched = words.match("Ada Lovelace")
cstr escapes = "\b\s"
// escapes contains the four bytes '\\', 'b', '\\', 's'.
// Its C++ spellings are "\\b\\s" or R"(\b\s)".

Ordinary escapes such as \n and \t still produce newline and tab characters. In an ordinary Cosmic string, use \x08 for a backspace; \b remains the regex word-boundary escape. Character literals, encoded strings, and strings originating in C++ headers retain their C++ escape rules. Lesson 05 checks both a match and the actual preserved bytes.

Embedded CSON, fields, and positional access

A map literal uses named keys and values. Bare keys are strings; values are Cosmic expressions, so an identifier in value position names a variable. Quote a textual value. Nested maps, [...] vectors, and explicit cvec{...} values can be combined directly to construct a cvar tree. This is embedded CSON with live Cosmic expressions; it does not parse a string at runtime. Standalone CSON files instead treat bare value names as symbols.

cvec labels = {"fast", "local"}
cvar options = {
  #: "job"
  retries: 3
  labels: labels
  nested: {enabled: true}
}
options:retries = 4
cvar retries = options:retries|10
bool hasRetries = options?retries
cvar message = {
  name: "Ada"
  scores: [10 20 30]
  nested: {enabled: true}
}
message:status = "ready"         // message["status"] = "ready"
cvar name = message:name         // message.at("name")
cvar retries = message:retries|3 // message.get("retries", 3)
// The default does not insert a retries field.

value:key looks up the literal string key with at(). It reports a missing field rather than inserting one. A simple assignment, value:key = expression, uses the container's inserting subscript operation. Compound assignments such as value:key += 1 first require the field to exist. Nested access follows the same rules at each level. A numeric colon suffix such as value:0 has a different meaning: it calls value.get<0>().

value?key calls has("key"). value:key|fallback calls get("key", fallback). The fallback expression is evaluated as an ordinary function argument even if the field exists; this is not lazy evaluation. A fallback applies only to that lookup: it does not protect a preceding missing parent in value:parent:child|fallback. For a computed key, ordinary at(key), get(key, fallback), and has(key) calls make the intent explicit.

#: "job" is the conventional message name. The Cosmic literal {:"job", retries: 3} is a shorthand for the same map. Standalone CSON files have their own value syntax; use the explicit #: job form in a database schema. Do not rely on the written order of map fields to sequence side effects.

A numeric dot suffix, such as values.0, accesses a position. It uses the container's subscript behavior, or the corresponding element of a std::tuple or std::pair. It preserves references, so an assignable element can be modified through it. Bounds checking depends on the underlying container; the shorthand adds none.

[i4] values = {10 20}
values.0 = 42
std::tuple<i4, cstr> pair{7, "seven"}
int first = pair.0
// values[0] is now 42; first is 7.

Include <tuple> for the tuple example. v.0 is positional subscript/tuple access; v:0 is the distinct v.get<0>() spelling and requires a type that provides that member template.

Symbol and function shortcuts

A leading backtick denotes a symbol or a named function value. `x means csym("x"); it does not look up a variable named x. `Foo(x, y) means cfunc("Foo", x, y): the arguments are ordinary evaluated expressions, stored as values, and no C++ function named Foo is called.

The parser recognizes both shortcuts, but the current native compiler rejects these value constructions. Use the explicit constructors in executable code, as lesson 31 does:

i4 x = 20
i4 y = 22
csym symbol("x")          // explicit equivalent of `x
cfunc request("Foo", x y) // explicit equivalent of `Foo(x y)
// symbol names "x"; request names "Foo" with arguments 20 and 22.

These are expression data that can be inspected, serialized, or explicitly sent to an interpreter or executor. They do not evaluate themselves. Backtick dispatch case names do compile: case `open is a name hash. A backtick after an object, as in person`name, has the separate property-access meaning described next.

Property reads and writes

A backtick explicitly requests a property getter. For example, object`name calls object.name(), and pointer->`name calls pointer->name(). Simple assignment requests the corresponding setter: object`name = value calls object.setName(value). The first letter of the property name becomes uppercase after set. Cosmic does not create the getter or setter; the object's interface must provide them.

Counter counter
counter`value = 7
int result = counter`value
Counter* pointer = &counter
pointer->`value = result + 1

Ordinary dot access can also select a property. If a known class has no data member named value, counter.value can call value(), and counter.value = 7 can call setValue(7). An actual data member takes precedence. Use the explicit backtick or a normal method call when a type is dependent or cannot be determined at that point.

Ordinary C++ type rules apply to these accesses, including aliases, inherited members, inferred local types, and the types of range-loop variables. The receiver is evaluated once. Constness, reference qualifiers, access control, deleted methods, and overload selection still apply. A private data member does not become a setter call merely because it is inaccessible.

The getter's return type controls copying and borrowing. A property read may execute code and have side effects; it is not automatically a stored field. Setter shorthand applies to simple assignment. To increment a property returned by value, write counter`value = counter`value + 1 or call the setter explicitly. Do not assume += or ++ invokes a setter.

Casts, moves, and concise calls

Cosmic puts named casts after their operand. Each form has exactly the conversion rules and failure behavior of its corresponding C++ cast:

CosmicC++ operation
value as Tstatic_cast<T>(value)
value as? Tdynamic_cast<T>(value)
value as! Treinterpret_cast<T>(value)
value as# Tconst_cast<T>(value)

For example, number as int converts a floating-point value to an integer. A failed dynamic cast to a pointer returns null; a failed dynamic cast to a reference throws. Reinterpretation does not validate an address, alignment, or object lifetime. Casting away constness does not make an originally const object writable.

struct Base{ virtual ~Base() = default; }
struct Test : Base{ i4 value = 42; }

Test test
Base* y = &test
checked := y as? Test*       // checked downcast; failure would be 0p
known := y as Test*          // valid because y really points to a Test
bytes := &test as! u1*       // reinterpret the address
restored := bytes as! Test*  // round-trip to the original pointer type
const Test* view = &test
mutableView := view as# Test*
mutableView->value = 53      // test was originally mutable

Lesson 30 also demonstrates a failing pointer dynamic cast and compares its result with 0p. The pointer casts above do not construct an object or transfer ownership.

Prefix &&value means std::move(value). It permits a receiving constructor or overload to move; it does not itself transfer anything. The moved-from object remains alive and follows its type's normal contract. Binary left && right is still logical AND.

cstr source = "hello"
cstr destination = &&source
source = "reused" // valid; the old contents of source are not promised

A leading dot abbreviates a Catalyst free-function name: .option(...) calls cOption(...), .args() calls cArgs(), .str(value) calls cStr(value), and .isFile(path) calls cIsFile(path). The first letter is capitalized and prefixed with c; this does not provide a method on an implicit receiver.

#include time
now := .now() // auto now = cNow()

For command-line options, register each option with .option(...) before constructing CProgram, then read the resolved arguments with .args():

#include core

int main(int argc, char** argv)
  .option("name:n", "Cosmic", "Name to greet")
  CProgram program(argc, argv)
  cvar# args = .args()
  print args["name"]
  %0

Braced aggregate initializers support C++23 member designators: Pair{.first = 20, .second{22}}. Designators follow member declaration order and may omit members, which use their default initialization. Nested braces, trailing commas, and anonymous union members are supported. A list cannot mix positional and designated initializers. Leading-dot calls such as {.str(value)} retain their usual meaning.

Language linkage declarations support extern "C" and extern "C++", for either one declaration or a braced group at namespace scope. Native C linkage interoperates with declarations from included headers. A single variable declaration has external storage; a braced group follows ordinary declaration rules. Standard asm("..."); declarations are accepted at namespace and function scope, using the target assembler's syntax.

Otherwise identical C and C++ function types are treated as the same type for function type identity.

function/value passes a single named value, and function/"text" passes a string. For example, cStr/value is equivalent to cStr(value). A trailing slash with no argument means an empty call. Use normal parentheses for numeric literals, compound expressions, or multiple arguments; numeric arguments such as function/42 do not currently compile correctly. Separate a division operator from its operands with spaces.

Power and bitwise XOR

With no whitespace on either side, x^y calls std::pow(x, y). The operation is right-associative: 2^3^2 is 2^(3^2), or 512. The result and numeric domain behavior follow the selected std::pow overload; an integer base does not imply an integer result.

f8 x = 2.0
f8 y = 3.0
f8 power = x^y // 8.0
i4 bits = 2 ^ 3 // 1: spaced ^ is C++ bitwise XOR

Keep the compact spelling for exponentiation. In ordinary Cosmic source, whitespace on either side of the caret selects XOR instead. Parenthesize compound operands explicitly, for example (x + 1)^y.

Counted loops and collection traversal

for count iterates over indices from zero through count - 1, with $ as the index. for(i: count) names the index explicitly, and for(i: begin, end) visits the half-open interval [begin, end). The index is a size_t and increases by one. Bounds are evaluated when entering the loop, not on every iteration. Use nonnegative bounds for these index forms; use an ordinary C++ loop for signed or decreasing iteration.

int total = 0
for 4
  total += $
// total is 0 + 1 + 2 + 3.

for(i: 2, 5)
  total += i
// Adds 2 + 3 + 4.
i4 total = 0
for(i : 10){ total += i; }
// total is 45; i visits 0 through 9.

for values uses the collection's span() when it provides one; for a vector this visits indices. To visit elements, write values| followed by a body. $ then refers to the current element as auto&, so assigning through it changes a mutable collection. Iteration follows the collection's own order.

CVector<int> values = {1, 2, 3}
values|
  $ *= 2
values|total += $

{cstr: int} scores = {{"first", 3}, {"second", 4}}
scores|
  print "${$k}: ${$v}"
  $v += 1
scores|print "${$k}:${$v}"

The last line combines traversal and printing. Interpolation needs ${...} around each expression, including $k and $v.

For pair-valued iteration, $k and $v refer to the current pair's first and second elements. A map's key normally remains const. Hash-map order is unspecified. Nested pipe loops introduce their own placeholders; give an outer value a name before entering an inner loop if you need both.

You can name both parts explicitly with for(KeyType key; ValueType value : map). Use reference types to avoid copying or to mutate a mapped value:

for(cstr# key; int& value : scores)
  value += 1
  print "${key}: ${value}"

These are ordinary loops, not callbacks. break exits the loop, continue advances it, and %value returns from the enclosing function. Iterator invalidation rules still apply: do not erase or structurally modify a collection in a way that invalidates its traversal. forever introduces an infinite loop, equivalent to for(;;), with the same control flow.

i4 attempts = 0
i4 maxAttempts = 3
forever{
  if attempts >= maxAttempts break;
  ++attempts;
}

[i4] values = {1 2 3}
while !values.empty() values.pop_back();

The abbreviated if and while each own one statement. Use a braced or indented body for several statements. The forever example checks its exit condition before incrementing, so it finishes with attempts == 3.

Lambdas and trailing arguments

Normal lambdas support typed and auto parameters, explicit template parameters, trailing return types, named captures, and [&] or [=] capture defaults. Nested lambdas and conversion to std::function are supported. Capturing by reference borrows a variable; a callback stored for later must not outlive its referenced objects.

If you omit a lambda's parameter parentheses, Cosmic supplies one auto& parameter named $. Include () when you intend a zero-argument callback:

auto twice = []{ %$ * 2 }
auto answer = [](){ %42 }
int value = 21
int result = twice(value)

The implicit reference parameter binds to an lvalue; choose an explicit by-value or forwarding-reference parameter if you need to accept temporaries.

A trailing call block containing $0, $1, and so on becomes a lambda passed as the last argument of the call. The surrounding variables are captured by reference. Each placeholder is an auto& parameter, and the highest index determines the total parameter count. Using $2 therefore creates three parameters even if the first two are unused.

#include <algorithm>

CVector<int> values = {1, 2, 3}
int total = 0
std::for_each(values.begin(), values.end()){
  total += $0;
}
[i4] values = {3 1 2}
std::sort(values.begin(), values.end()){
  return $0 < $1;
};
// values is now {1, 2, 3}.

This supplies the comparator as the third argument to std::sort, like [&](auto& a, auto& b){ return a < b; }. The sort finishes before returning, so its borrowed captures need only remain valid for that call. A storing API requires a longer lifetime; use an explicit lambda when you need different captures or parameter types.

A return in this block returns from the callback. There is no automatic Boolean result for a general trailing callback; supply a result when the called API expects one. Without numbered placeholders, an arbitrary trailing code block is not automatically a zero-argument lambda. Pass an explicit [&](){ ... } instead. Database callbacks have their own rules described below.

A trailing map is different: it supplies an ordinary final argument. send(){#: "ready", count: 3} calls send with a map value. It does not run a callback, and normal argument evaluation and copying rules apply. Similarly, new Widget{width: 640, height: 480} passes a map to the constructor, which must accept it. Use braces containing positional values, or normal parentheses, for ordinary C++ construction.

void send(cvar& received, cvar# message){
  received = message;
}

cvar received
send(received){
  message: "hello"
};
// Calls send(received, cvar{...}); received now has a message field.

Dispatch without implicit fallthrough

dispatch selects a case like a switch, and ends each nonempty case automatically. Parentheses and braces are optional; indent the cases and their bodies as in the examples below. Empty cases can share the following body. An explicit break is unnecessary for a completed case; an ordinary switch still requires the usual C++ control flow to avoid fallthrough.

int result = 0
dispatch 2
  case 1
    result = 10
  case 2
    result = 20
  default
    result = -1
// result is 20; execution does not continue into default.

The selector may be an integer or enum, or a value with a Catalyst hash(), such as cfunc. For strings, dispatch on cHash(text) explicitly. A backtick before a case name hashes the name: case `open compares with cHash("open").

cstr command = "open"
int result = 0
dispatch cHash(command)
  case `open
    result = 1
  case `close
    result = 2
  default
    result = -1

To dispatch a function by name and argument count, use a hash literal with its arity:

cfunc request("scale", 7, 6)
dispatch request
  case "scale"2#
    print request[0] * request[1]
  default
    error "unknown operation"
csym command("open")
dispatch command
  case `open
    print "opening"
  default
    print "unknown command"

cfunc request("Foo", 20 22)
dispatch request
  case "Foo"1#
    print "one argument"
  case "Foo"2#
    print request[0] + request[1]
  default
    print "unknown function"
// The second dispatch prints 42.

A function selector's case uses its name and argument count, not its argument values. A symbol selector uses its name. Lesson 31 checks both forms and the absence of implicit fallthrough.

Use "scale"2# for this case; the older `scale:2 spelling currently fails to parse. Hash dispatch compares hashes only. Where arbitrary external strings must be distinguished despite possible collisions, use explicit string comparisons.

Message handlers and dynamic executors

A class derived from CHandler can group message methods under handlers:. Every handler must take one named cvar& parameter. A message is a map whose "#" field names the handler. The argument is the original message, so a handler can read or change its other fields.

#include core

class Events: public CHandler{
public:
  int total = 0
handlers:
  add(cvar& message)
    total += message:amount.getInt()
}

// Inside a function:
Events events
cvar message = {#: "add", amount: 42}
bool handled = events.handle(message)

Cosmic supplies the public handle(cvar&) entry point. It returns true after calling a recognized handler. This Boolean reports recognition, not the handler's return value or application success; exceptions still propagate. An unknown message is delegated to an inherited handler, or returns false when no handler recognizes it. Supply a valid string "#" field; missing fields or invalid payload values are not an automatic validation scheme.

For function-shaped requests, derive from CExecutor and declare callable methods under executors:. Cosmic supplies execute(const cfunc&) and a recognition check accessed through CExecutor:

class Calculator: public CExecutor{
executors:
  int scale(int value, int factor = 2) value * factor
  clear(){}
}

// Inside a function:
Calculator calculator
CExecutor& executor = calculator
cfunc request("scale", 21)
if executor.canExecute(request)
  cvar answer = executor.execute(request)

Selection uses the method name and argument count, including counts allowed by default arguments. Request arguments are converted from cvar to the declared parameter types, and results are returned as cvar. A void method returns cnone. canExecute() checks name and arity; it does not guarantee that argument conversions or the method body will succeed. execute() reports an unrecognized request as an error.

Use distinct names or argument counts for dynamic overloads; requests do not select an overload by examining value types. A method taking cargs const& receives the complete argument vector when it needs to interpret a variable number of values itself. These calls are synchronous calls on the object. They do not by themselves queue work or send a network message. The abbreviated object!method(...) syntax is not yet supported by native compilation; use execute(cfunc(...)).

Deferred, storable, packed, and forwarded methods

defer on a function or method separates its declaration from the placement of its definition. It lets a class keep a method's body alongside its declaration while arranging for that body to be defined later. Calls still execute immediately, with ordinary C++ return and exception behavior. This is not a scope-exit action or asynchronous execution.

struct Counter{
  int value = 0

  defer int increment()
    %++value
}

storable on a class or struct supplies a constructor taking CBuffer& and a void store(CBuffer&) const method. The constructor reads each nonstatic data member in declaration order; the store method writes the same members in the same order. Provide your normal construction path separately:

#include <mc/CBuffer.h>

storable struct Stored{
  int first
  int second
  Stored(int a, int b) : first(a), second(b){}
}

// Inside a function:
Stored original(12, 30)
CBuffer buffer
original.store(buffer)
buffer.rewind()
Stored restored(buffer)

Member types must support the corresponding buffer storage and restoration operations. Static members are excluded. Base-class state is not automatically serialized, and pointer values are not a scheme for persisting an object graph. You can supply your own store method or buffer constructor instead of its generated counterpart. The format has no automatic versioning: changing field order or types changes what is read and written. Preserve that contract for saved data, or define an explicit versioned format.

packed class and packed struct request byte alignment and remove padding between ordinary fields. Named bit-fields use C++ syntax, such as u8 rowId : 63, and widths may depend on template parameters. Packing follows the target's C++ layout. It neither changes the serialization rules of storable nor makes a binary format portable across architectures. Avoid treating a potentially unaligned member address as a normally aligned pointer.

outer_methods: is an authoring convenience for a class split into an outer interface and a separate implementation. A class named Counter_ can supply definitions for corresponding methods already declared by Counter. The implementation has an o_ pointer to its outer object; the outer class owns it through x_. Methods in the labeled region generate forwarding definitions with their parameters, return values, and const qualification preserved.

// In Counter_, with int add(int) declared by Counter:
outer_methods:
  int add(int amount)
    value_ += amount
    %value_

Cosmic also supplies the outer destructor that releases x_. Declare that destructor in the outer interface, and do not provide a second definition. The outer constructor remains your responsibility, as do copy and move ownership rules. A forwarding declaration does not make copying an owning pointer safe. Use this feature when authoring such a class; callers use its normal public interface and need no special syntax.

Scoped guards, output, and errors

guard(lock), readGuard(lock), and writeGuard(lock) hold a lock for the following block. Include <mc/thread.h>. The first form uses lock()/unlock(); the others use readLock()/readUnlock() or writeLock()/writeUnlock(). Catalyst's CMutex and CRWMutex provide the corresponding interfaces. The lock object must remain alive for the whole block.

#include <mc/thread.h>

int takeNext(CMutex& mutex, int& next)
  guard(mutex)
    %next++
CMutex mutex
i4 i = 0
guard(mutex){
  ++i;
}
guard mutex ++i;
guard mutex
  ++i
// i is now 3; each guard has already released the mutex.

Parentheses may be omitted before a single statement or an indented body. The current parser requires them with an explicit braced body: write guard(mutex){ ... }, because guard mutex{ ... } is currently rejected. The shorthand statement guard mutex ++i; holds the lock only for that increment. Lesson 32 also reacquires a lock after a guarded error to demonstrate release during exception unwinding.

The lock is released when the block ends, including on return, exception, break, or continue. A read guard does not make arbitrary writes safe, and these constructs do not change a lock's recursion or upgrade behavior.

StatementBehavior
print valueWrite to standard output, append a newline, and flush.
output valueWrite to standard output without adding a newline or an explicit flush.
log value, np(value)Write to standard output with source filename and line. Nonliteral expressions include their expression text and value.
npe(value)The same diagnostic output on standard error.
error "message"Raise a Catalyst CError; normal exception handling applies.
assert condition, assert(condition) Evaluate once and terminate on failure with source information. NDEBUG disables the check and its evaluation.

Output operations use Catalyst's output synchronization for an individual operation. Several separate output statements are not one atomic message. Use an interpolated string when a complete line should be written together. Assertions are for invariants; use exceptions or explicit result checks for errors that a caller must handle.

cstr name = "Ada"
print "hello ${name}"     // newline and flush are added
output "hello ${name}\n" // newline is explicit
log 22                  // diagnostic output includes source information
i4 x = 3
assert x > 2

try
  error "an error occurred"
catch(CError& failure)
  print failure.what()

Keep required work outside an assertion because Release builds commonly define NDEBUG and omit its evaluation. The tutorials use active check() calls alongside assertions so their demonstrated results are still checked in Release.

Database, GPU, and compile-time features

Cosmic has dedicated syntax for generated database rows and indexed traversal. Item row{name: "example"} and Item row = {name: "example"} initialize a generated row through its setters; table:get(row; Id(42); name) performs an indexed lookup with a field selection, and table:query(Id(42)){ ... } runs a callback with the row named $. Unlike a pipe loop, the traversal block is a Boolean callback: %false stops traversal and reaching its end continues. Writes still require the table's update/erase and commit operations. The database section covers schemas, index ordering, projections, and callback lifetimes.

// Person is generated from the adjacent database.cson schema.
Person person{name: "Fred", score: 53.0}
// The same syntax with an age field in the schema is:
// Person person{age: 53, name: "Fred"}

Person ada{id: 1, name: "Ada", score: 42.0,
  notes: {language: "Cosmic"}}

Field names must belong to the generated row class. These initializers invoke generated setters and can set selected fields without following their storage order. They construct a row value; they do not insert it into a table. Lesson 14 follows initialization with insert(), commit(), and a persistence check.

gpu marks a GPU function, kernel marks a GPU entry point, and hybrid makes a function available for both CPU and GPU output. This selects where code is compiled; it does not automatically transfer memory or launch a GPU job. See Cosmic GPU programs for Metal output, thread indices, runtime requirements, and compilation commands.

CUniqueId(key) assigns a zero-based number from a compiler-maintained counter for that literal key, and CUniqueCount(key) reads the current count without incrementing it. Separate keys have separate counters. These are compile-time values within a compilation, not a runtime counter or a globally unique identifier service:

int kindCount()
  s8 First => CUniqueId("localKinds")
  s8 Second => CUniqueId("localKinds")
  s8 Count => CUniqueCount("localKinds")
  %Count

With these declarations processed in order, the values are 0, 1, and 2. Keep these calls inside function bodies in native code; namespace-level constant initializers do not yet support them. Keep uses together when their ordering matters; rearranging declarations or compiler transformations can change the assignments. Do not use them as stable wire, storage, or cross-file type codes.

Current limits and retained syntax

Cosmic is still a growing subset and extension of C++23. Native compilation supports ordinary functions and overloads, classes, constructors and destructors, local classes, aliases, templates, arrays, packed records, control flow, exceptions, and the supported extensions above. Imported C++ headers can provide additional C++ features without conforming to Cosmic's source grammar.

CPU compilation checks declarations, types, overloads, templates, access, and conversions according to C++23 rules. Diagnostics identify the original source file and line; included C++ headers participate in those same checks. Cosmic also diagnoses invalid uses of its own language extensions.

Native compilation supports static_assert at namespace, class, and block scope, block-local type aliases and enums, declarations in conditions, and unnamed typed exception handlers. thread_local variables retain separate state per thread, and alignas is checked and applied to variables, fields, and records. Braced member initialization uses C++ list-initialization rules, including initializer-list constructor selection.

Standard attributes use [[...]] lists on declarations, types, and statements. This includes nodiscard, deprecated, maybe_unused, no_unique_address, noreturn, carries_dependency, fallthrough, likely, unlikely, and assume. Attributes are checked at their written position. Unknown vendor attributes are ignored. [[assume(condition)]]; does not evaluate the condition's side effects; the program must satisfy its assumption whenever control reaches that statement.

Lambda attributes before the parameter clause apply to the call operator. An attributed lambda that omits the parameter clause keeps Cosmic's implicit parameter, for example [] [[nodiscard]] { return __; }.

Native compilation also supports attributes on assembly declarations, keyword attribute names such as [[using]], and prefix attributes on non-type template parameters.

Selection and range-for statements can have an init statement, for example if(int value = read(); value > 0){ ... }. Switch labels keep their source order and share the enclosing switch scope; use braces when a case needs a separate scope. Labels and goto follow C++ lifetime and initialization checks. Conditions also accept calls with pointer or reference arguments, including if(check(&value) != 0), and comma expressions. consteval, constinit, and if consteval are supported, including the negated form if !consteval.

Cosmic's pair traversal for(Type key; Type value : collection) retains its existing meaning. To use a range-for init statement, give its variable an initializer or use an expression statement. This avoids the syntax reserved for pair traversal.

Expressions support unary +, three-way comparison <=>, .* and ->*, alignof, typeid, noexcept, and sizeof.... Alternative operator keywords such as and, not, and bitand have their C++ meanings. Parentheses are retained where they affect types: decltype((value)) preserves the expression's value category, while decltype(value) can name its declared type.

Fundamental type specifiers include signed, unsigned, short int, long int, long long int, long double, and the standard character types. Qualifiers apply at their written pointer level: volatile int* points to a volatile integer, while int* volatile is a volatile pointer. Nested pointer qualifiers and more than three pointer levels are supported.

Out-of-class definitions can repeat template heads for enclosing classes and member templates, including their requires clauses. Nested class definitions, member variable templates, conversions, constructors, destructors, and partial or explicit specializations keep the same template scope and lookup rules as C++.

Type aliases and parameters can describe function pointers, member pointers, and multidimensional arrays. For example, using Callback = int(*)(int) names a function-pointer type, while using Matrix = int[2][3] preserves both array extents. Reference-to-array parameters retain their bounds. typedef declarations are supported at namespace, class, and block scope, including comma-separated aliases.

Parenthesized variable names work with aliases and qualified types, including Integer(value); and Names::Integer(value){42};. Type lookup distinguishes these from Cosmic function declarations that omit the return type.

If Name names a class or enum and Argument is a type, Name(Argument); retains Cosmic's omitted-return function meaning. An empty brace body has the same interpretation. Use Name object; or Name(object) = {}; to declare a variable in this overlap. C++ header macros keep the C++ declaration meaning.

Class, union, and enum definitions can declare objects or aliases immediately after the closing brace, for example struct Pair{ int first, second; } value{20, 22}, *pointer = &value;. The declarators share the defined type. Anonymous definitions also work in typedef and ordinary using aliases, including pointers and arrays. An anonymous union with a trailing object name exposes its members through that object.

Comma-separated variable declarations retain a separate type and initializer for each name, as in int value = 42, *pointer = &value. Multidimensional array variables support nested braced initializers. Direct braced initialization is also available for namespace variables.

Declaration groups can mix functions and variables, as in int compute(int), value = 42. Parenthesized declarations follow C++ type lookup: Value value(Count()) declares a function when Count is a type. Use extra parentheses, such as Value value((Count())), to force object initialization. Block function declarations are also supported.

A function-type alias can declare a function, for example using Operation = int(int); Operation increment;. This also works for member functions and block declarations. Define the function with an explicit parameter list.

Namespaces support aliases, inline namespaces, and nested definitions such as namespace library::detail{ ... } and namespace library::inline version{ ... }. Namespace aliases and using-directives can also appear inside functions. Opaque enum declarations such as enum class Mode; and enum Code : unsigned short; can be completed later. A nested enum can be defined outside its class.

Using-declarations import names and overloads, including conversion functions and inherited constructors. They support comma-separated names, typename, and pack expansion. using enum State; makes an enum's members available in the current namespace, class, or block.

Structs inherit publicly by default; classes inherit privately. Virtual bases share their subobject through multiple inheritance. Base lists can expand template packs, and a class or struct marked final cannot be used as a base.

Friend declarations grant access to a class's private and protected members. They support class and function templates, individual specializations, type aliases, and qualified member functions, constructors, destructors, and conversions. An inline friend defined in a class is a namespace function and can be found through argument-dependent lookup.

The current compiler cannot enforce friend declarations that grant access to members of every specialization through a templated qualifier, such as template<class T> friend class Outer<T>::Inner;. These declarations produce a diagnostic.

Named unions support constructors, destructors, methods, aliases, templates, and alignment. Anonymous unions make their members available in the surrounding class or function; at global or named namespace scope, declare the anonymous union static. Explicit destructor calls such as item.~Item() support manually managed union members.

Native compilation supports thread-local anonymous unions.

Bit-fields support constant-expression widths, unnamed padding, zero-width separators, default member initializers, and declaration groups. For example, unsigned code : 6 = 42, : 0; declares a six-bit field followed by an allocation-unit boundary. Their layout follows the target C++ implementation.

Recursive declarators can also appear in local declarations and function returns, for example int(*callback)(int) and int(&values())[3]. Functions and methods can return function pointers or array references, and pointer parameters can be packs. Native name lookup resolves local declaration-versus-call ambiguities.

Parenthesized names such as int(value) = 42; and local declarations through a type alias are supported. Function pointers accept direct initialization, for example int(*callback)(int)(increment);. Elaborated type spellings such as struct Forward* can introduce a forward declaration or select a class hidden by a value of the same name. They also work in parameters, aliases, and explicit instantiations. Qualified calls such as object.Base::read() select the named base implementation. Bit-field widths can construct temporary values, including Width{}.value.

Function signatures support trailing return types, conditional noexcept and explicit, member cv and ref qualifiers, override and final, and defaulted or deleted declarations. Conversion functions can be defined in or outside their class. Default arguments and exception specifications can refer to members declared later in the class.

Function try blocks put try before the body, or before a constructor's member initializer list. Their handlers cover the body and constructor initialization or destructor cleanup. Function parameters remain available in the handlers. Falling off the end of a constructor or destructor handler rethrows the exception; an ordinary function handler can return a result. Put try on the signature line to distinguish it from an ordinary try statement inside an indented Cosmic body.

Allocation expressions support placement arguments, explicit global lookup, parenthesized types, and braced or parenthesized initializers. Array allocations support a dynamic first bound, constant inner bounds, and deduction from an initializer, as in new int[]{20, 22}. Custom allocation and deallocation functions retain overload selection and cleanup after a constructor throws. Use ::new or ::delete to request global lookup.

The current compiler rejects new A{20, 22} when A is an alias for int[]. Use new int[]{20, 22} for that allocation. On the current macOS target, catch std::bad_alloc to handle a negative runtime bound; the compiler currently throws that base exception rather than std::bad_array_new_length.

Explicit instantiations request a concrete template definition, as in template class Buffer<int>; or template int identity<int>(int);. Put extern before template in other translation units to use that definition. Class, function, variable, and member instantiations retain template constraints and overload matching. An instantiation belongs at namespace scope and cannot provide an initializer or function body.

Fold expressions such as (values + ... + 0) combine parameter packs using C++ operator and empty-pack rules. Unary and binary folds support left and right association, short-circuiting, and overloaded operators. Put a compound fold operand in parentheses, as in ((values * 2) + ...).

Template-template parameters accept class and alias templates, defaults, nested parameter lists, and packs. For example, template<template<class> class Container> lets a definition use Container<int>. Template arguments can be empty to select defaults. Dependent template names retain their template qualifier, as in T::template Container<int>.

Structured bindings decompose arrays, tuple-like values, and class members. Use auto [first, second] = value; for a copy or auto& [first, second] = value; to refer to the original elements. Const and rvalue references, static and thread-local storage, and tuple customization through get are supported. Bindings can appear in range-for declarations and in the init-statements of if, switch, and for.

Numeric literals support digit separators, hexadecimal floating point, large unsigned values, and C++23 size suffixes such as 42uz. Suffixes select their C++ types, including long double. Numeric literal operators can be declared with operator""_suffix, including templates that receive the digits as a character pack. Standard chrono literals work when their namespace is in scope.

Raw strings, wide and UTF strings, encoded characters, C++23 escape forms, and string or character literal operators are supported. Adjacent literals preserve their encoding and length, including embedded null characters. Raw strings such as R"(${value})" keep their contents literally.

Ordinary strings retain Cosmic's interpolation and regex escapes. In particular, \b and \? retain their backslashes in ordinary strings; use \x08 for a backspace character in one. Encoded strings and character literals use the C++ meanings of those escapes.

Functions can use C-style variadic arguments, parameter packs, unconstrained auto parameters, and explicit object parameters such as int get(this const Value& self). Handlers and executors can use explicit object parameters; dispatch supplies the object and counts only the ordinary arguments. Trailing returns can refer to the preceding parameters, as in auto sum(int a, int b) -> decltype(a + b).

Lambdas support init-captures such as [value = expression] and [&alias = original], capture packs, and [*this] to capture an object snapshot. Use mutable to modify values captured by copy. Lambdas also support constexpr, consteval, noexcept, and explicit object parameters. A static lambda has no captures. Generic lambda parameters can be packs, and an explicit template head can carry a parenthesized constraint.

The following forms are not supported by CPU compilation. Use the alternatives shown:

Retained formMeaning and current alternative
42vA numeric cvar literal. Use cvar(42).
`x, `Foo(x, y)Symbol and function values. Use csym("x") and cfunc("Foo", x, y). Backtick dispatch case names are supported separately.
int foo(int x) if x > 2 %x * 2A conditional directly after a function signature. Put the conditional in an indented or braced body and return on each required path. Single-expression function bodies do compile.
object!method(...)Dynamic execution. Use object.execute(cfunc("method", ...)).
forge Type(...)Construction through the registered object factory, yielding a cvar. Use the runtime object APIs explicitly.
Container for count expression, Container for| range expressionCollection construction from an indexed or element traversal using $. Use an explicit result container and loop.
discrete Name : Type { ... }An enum-like value type with named constants and string conversion. Use a regular enum and explicit conversion functions for native code.
task { ... }, cfor(...){ ... }, arbitrary unrecognized call blocksNo native execution support. Use explicit calls, loops, and lambdas.

Cosmic .hh header translation is still incomplete. Swift generation and phases have been removed.

Use the supported database query blocks and numbered-placeholder callbacks described above; they are distinct from unrecognized trailing block forms. When a construct is rejected, the diagnostic's original source location identifies the code to adjust. Debug builds also preserve source locations for supported functions, templates, and callbacks; see debugging Cosmic.

Database rows and queries

Cosmic can generate typed rows for CDatabase from a CSON schema. Begin the map with #type: database to enable automatic generation. Each remaining top-level key names a row class and its table. The fields vector specifies the fields in their storage order; indices lists indexed fields or composite indices:

{
  #type: database
  Item: {
    fields: [
      {#: id type: u4 unique: true}
      {#: group type: u4}
      {#: rank type: f8}
      {#: name type: str optional: true}
      {#: payload type: var optional: true}
    ]
    indices: [id rank [rank group]]
  }
}

Place the schema beside the source files that use it. In a CMake application, call catalyst_cosmic_directory("${CMAKE_CURRENT_SOURCE_DIR}") after defining the targets. The SDK's integration generates matching headers in the build directory and supplies their include paths. For example, examples/14-database/database.cson produces database.hxx; include it with #include "database.hxx" from either C++ or Cosmic.

Multiple schemas in a directory are supported: users.cson produces users.hxx, and orders.cson produces orders.hxx. The marker must be the first field in the map; quoted "#type": "database" also works. Other CSON files are left alone. New schemas and schema edits are picked up on the next build. A failed generation stops the build and preserves the last valid header.

Keep the CSON file as the source of truth. Remove any old generated .hxx with the same name from the source directory when adopting automatic generation, so quoted includes cannot select a stale copy. Each build directory owns its generated headers; do not edit or commit them.

For manual generation without CMake:

mc-cosmic --database -o app_database.hxx app_database.cson

The original -database spelling also works. Without -o, this example writes app_database.hxx in the current directory. Explicit --database also accepts older schemas without the marker. Generation reports invalid types, names, and index definitions, and preserves an existing output on failure.

Field types are b1 (Boolean), u1/i1/u2/i2/u4/i4/u8/i8 (unsigned/signed integers with the indicated byte width), f2/f4/f8 (floating point), str, and var. Rows use 32-bit row IDs by default; add big: true to a table for 64-bit row IDs. The database assigns row IDs separately from application fields such as id.

unique: true rejects duplicate indexed values. optional: true permits an indexed field to be absent. hash: true retains the index hash while removing that field's original value from the stored row. Preserve field order, types, and index definitions when reopening an existing table.

Include CDatabase before the generated header. The header contains row declarations and can be included inside a namespace. Create or open the actual table with the generated type:

#include core CDatabase
#include "app_database.hxx"

// Inside a program with a live CProgram and CDatabase:
auto table = database.createTable<Item>()
Item row = {id: 42, group: 7, rank: 3.5, name: "example"}
row`payload = cvec{1, true, "data"}
table->insert(&&row)
table->commit()

Item found
if table:get(found; Id(42); name payload)
  std::cout << found`name << '\n'

A field named name provides Name, setName(value), name(), and hasName(). Backtick access uses these getters and setters. A local row's map initializer assigns its named fields through setters; the map does not specify an evaluation order for field expressions. Strings and cvars have both copying and moving setters. The generated Fields and Indices aliases remain available for direct use with the CDatabase C++ interface.

table:get(row; Id(value)) returns whether it found a row through the Id index. Use RowId(value) to fetch by the database-assigned row ID. An optional final semicolon introduces the fields to load, separated by spaces or commas. Omit that list to load every field. Unselected fields are absent in the returned row; the database-assigned row ID remains available.

table:forward(){
  std::cout << $`name << '\n'
}
table:backward(Rank; name){
  std::cout << $`name << '\n'
  %false
}
table:query(Rank(2.0, 5.0); name rank){
  std::cout << $`name << " " << $`rank << '\n'
}

Without an index, forward() and backward() traverse by row ID. For projection without an index, write forward(name payload). An index name selects index order; query(Index(value)) selects an equal value, and a numeric index also accepts an inclusive start/end range. The table receiver can be a smart pointer, a raw pointer, or a reference to either.

Inside a traversal, $ is a reference to the current row. Reaching the end of the callback continues traversal; %false stops, and %true continues immediately. A return exits the callback, not the enclosing function. Update or erase rows through the table, then commit after traversal:

table:query(Id(42)){
  $`name = "changed"
  table->update($)
}
table->commit()

table:query(Id(42)){ table->erase($) }
table->commit()

A composite index such as [rank group] provides the name RankGroup. Its first field supplies ordering; the remaining fields select the group. For example, table:get(row; RankGroup(3.5, u4(7))) finds that rank in group 7, and table:forward(RankGroup(u4(7)); name){ ... } traverses that group's rows. These operations follow the same CDatabase commit and persistence rules as direct C++ calls. Generate the schema header before compiling a source that includes it; use -I if it is in another directory.

For a named callback passed to the table's C++ methods, the original cquery form is also available. Its named row parameter is a reference, and its body must return a Boolean on every path:

auto callback = cquery(Item, row){
  std::cout << row`name << '\n'
  %true
}
table->traverseForward(callback)

The compiler locates its bundled resources, headers, and libraries relative to the SDK. Keep that directory layout intact. To select locations explicitly, use --resources DIR, --sdk-include DIR, --library FILE, or --sysroot DIR. This release supports macOS on Apple Silicon; Linux support is coming soon.

Cosmic GPU programs

Mark GPU functions with gpu and entry points with kernel. hybrid retains a declaration for both CPU and GPU use where its body is valid in both environments. GPU entry points have access to conveniences such as ThreadId. For example:

gpu float squared(float x)
  %x * x

kernel void fill(float* values)
  values[ThreadId] = squared(2.0f)
mc-cosmic --emit-metal \
  -o kernels.metal kernels.cc
mc-cosmic --metallib \
  -o kernels.metallib kernels.cc

Metal source generation uses the supplied Cosmic GPU support library. Building a .metallib requires macOS, Xcode, and Apple's Metal toolchain. If Xcode reports that the component is missing, install it with xcodebuild -downloadComponent MetalToolchain. Generated Metal source retains original file and line directives.

GPU vector aliases such as f4x4 select Metal's vector types directly. Available element types and lane counts depend on the Metal toolchain and target. An alias for a reserved wide or double vector type does not enable that unsupported type.

The runnable tutorial in examples/27-compute/main.cc defines a gpu helper, a kernel entry point, and its CPU main() in one Cosmic file. Its CMake target compiles the GPU declarations with --metallib and the host code as an ordinary executable. CGLRun loads the resulting library, binds the host vector, dispatches the kernel, and copies the results back. After configuring the tutorial project, build and run it on macOS with:

cmake --build examples-build --target example-compute
./examples-build/example-compute

Compile the CPU executable and GPU library separately. Existing CGL_ run/interact calls are retained. The application must supply the CGL_ declarations and implementation, including its library-loading and dispatch behavior; this compiler does not provide that runtime or automatically load a generated Metal library. Use object output and your normal C++ link command when additional application libraries are needed.

Debug Cosmic source

Compile with -g -O0 to keep source locations and local variables. Breakpoints and stack frames refer to the original Cosmic .cc source. On macOS the compiler creates an adjacent .dSYM bundle for an executable; keep that bundle and the matching source files available.

lldb ./hello
(lldb) breakpoint set --file hello.cc --line 6
(lldb) run
(lldb) next
(lldb) bt
(lldb) frame variable

Object files produced with -c -g carry their own debugging information. Optimized builds can inline calls and remove variables. Launching a program under the debugger requires the host's debugger permissions.

Build and run Microcosm programs

The mc-microcosm command compiles Microcosm source into a native executable or object file. It supports classes, constructors, methods, default arguments, recursive functions, collections, references, string interpolation, conditionals, and loops, subject to the limits below.

The compiler is included in the SDK. After configuring your environment, save this small program as hello.mc:

import cms

values := [3, 5, 8]
total := 0
values|total += $
print "Total: ${total}"
mc-microcosm -g -O0 -o hello-microcosm hello.mc
./hello-microcosm

# Produce an object file instead of linking an executable.
mc-microcosm -g -c -o hello-microcosm.o hello.mc

Both the compiler and generated programs use Catalyst's CProgram conventions, including MC_HOME. Select optimization with one of -O0 through -O3; the default is -O0. Use --emit-llvm -o program.ll to write LLVM IR. Failed compilation preserves an existing output file.

The compiler locates its headers and libraries in the SDK. Override resource selection with --sdk-include DIR, --library FILE, and --sysroot DIR.

Native compilation does not yet support lambdas, comprehensions, switch and try/catch statements, declaration modifiers, overloaded global functions, or imports other than cms. Unsupported constructs report errors. This release supports macOS on Apple Silicon; Linux support is coming soon.

Debug Microcosm source

Compile with -g -O0 to retain source locations and local variables with optimization disabled. Breakpoints and stack frames refer to the original .mc file and its line numbers, including expressions inside interpolated strings. Runtime helper frames may also appear when stepping into framework operations.

lldb ./hello-microcosm
(lldb) breakpoint set --file hello.mc --line 5
(lldb) run
(lldb) next
(lldb) step
(lldb) bt
(lldb) frame variable

On macOS, an executable built with -g gets a companion hello-microcosm.dSYM bundle. Keep it with the executable; the compiler's temporary object is not needed for debugging. Debug object files produced with -c -g retain their own DWARF information. The SDK includes the tool needed to produce these macOS debug bundles.

Keep the original source files available to the debugger. If they move, use LLDB's settings set target.source-map OLD_ROOT NEW_ROOT. Higher optimization levels can inline calls, remove variables, and make stepping less direct even with -g. Interactive stepping requires a host that permits debugger launches.

When Catalyst's error-stack capture is enabled, uncaught CError exceptions also use these source locations in stack traces. Failures inside native Microcosm functions then identify the original file and call-site lines.

Parse Microcosm source

mc::CMParser reads Microcosm programs and expressions into cvar syntax trees. Include <mc/CMParser.h> and link Catalyst::Shared. Parsing describes the program; it does not execute it.

mc::CMParser parser;
mc::cvar program = parser.parseFile("simulation.mc");
mc::cvar expression = parser.parseExpr("1 + 2 * 3");
// expression is Add(1, Mul(2, 3)).

parse(text) reads a complete program from a mc::cstr or a NUL-terminated string. The result is a Block whose first entry is Module(name) or PrivateModule(name). Declarations and statements follow in source order. parseExpr(text) returns one expression. Imports are retained in the tree; parsing does not require imported modules to be installed.

The parser accepts the existing indentation-based language, including classes, functions and default arguments, control flow, lambdas, collection literals, property access, and string interpolation such as "value ${a + 2}". Switch cases contain expression trees: Switch(condition, [Case(expression, block), …], defaultBlock). The default is none when absent.

Integer literals may use decimal, a leading zero for octal, 0b for binary, or 0x for hexadecimal. Invalid digits, missing digits after a prefix, and overflow are errors. Binary, octal, and hexadecimal literals can express all 64 bits; for example, 0xffffffffffffffff represents -1.

Function and constructor parameters must have distinct names. Once a parameter has a default value, every following parameter must also have one. Lambda arguments $0 through $14 are detected inside collection literals as well as ordinary expressions. Nested lambdas have their own arguments, and each lambda requires its closing }.

Map keys may use either single or double quotes. Block comments /* … */ may appear between expression tokens. Keywords match complete names, so a name such as elsewhere remains an ordinary identifier. Indentation determines which switch owns a case, including when switches are nested.

A parenthesized comma expression, such as (count += 1, count + 2), evaluates its left side first and returns the right side's result. It can be passed as a single function argument with f((a, b)); f(a, b) passes two arguments.

Use setFileId(id) before parsing when combining several files. Statements carry the file ID in tag().kind and the source line in tag().attrs. tokens() exposes the most recent parse's token offsets and categories. Copy tokens if you need them after the next parse. discardComments(false) retains standalone comments as syntax-tree entries.

Expression nodes also carry source lines where available. Multiline statements retain their starting line, and expressions inside ${…} refer to lines in the enclosing source file. An escaped \n inside a string does not advance the source line. parseExpr() rejects additional statements following the expression.

Malformed input reports CParseError, including a line number and, for parseFile(), the path. A parser can be reused after either success or failure. Each parse starts a new module. Source files are limited to 16,777,215 bytes and individual tokens to 65,535 bytes. Excessive nesting and numeric literals outside their supported range are rejected. Use separate parser objects concurrently.

Microcosm values and helpers

Microcosm uses Catalyst values for scalars, collections, expressions, objects, and callable values. Its core helpers include printing, string conversion, mathematics, filesystem operations, environment variables, program options, random numbers, and calendar conversions.

Calendar functions that fill several outputs require references to variables. Pass each output with &, as in .getDayInfo(2024 2 29 &dayOfYear &dayOfWeek). Invalid output arguments are rejected before changing any outputs. Epoch conversions use the same local-time conventions as Catalyst's date and time functions.

Distance requires vectors of equal length. Character predicates return false for an empty string and otherwise examine its first byte. A loop over an integer count visits the indices from zero through count - 1; a nonpositive count visits no indices.

<mc/CMInterpreter.h> supplies the CMInterpreter<I> base for custom interpreters. Get(), Put(), and Push() retain references when operating on interpreter variables; those references remain valid only while their targets exist. Results computed from temporary containers own their values. Its host-side cGridSize() helper currently throws because no GPU execution grid is available to the interpreter.