Melt

A minimal interpreted, object-oriented programming language built in C++.

Overview

Melt supports variables, arithmetic, strings, arrays, control flow (if/else, for, while), try/catch/throw, classes and methods, return, and import. It includes built-ins for file I/O, JSON, HTTP server, optional MySQL, encoding (Base64, XOR), and Blade-like HTML templates. The examples/web_project_mvc/ project shows a full MVC-style web app; examples/official_website_using_melt/ is the full official Melt website with Home, Documentation, About, Resource, Support, and a dynamic Blog.

Melt language features for web: Built-in HTTP server (no separate process); one handler with handle(), setHandler("App"), listen(port). Request/response API: getRequestPath(), getRequestMethod(), getRequestBody(), getRequestHeader(name); setResponseBody(), setResponseStatus(), setResponseContentType(), setResponseHeader(name, value); streaming via streamChunk(str) and sleep(seconds). Static file serving: servePublic(path) serves from public/ under /js/, /css/, /images/ with correct Content-Type; call it first in your router—if it returns true, the request is handled.

Quick reference

  • Run: ./build/melt script.melt
  • Build: cmake -B build && cmake --build buildbuild/melt
  • With MySQL: cmake -B build -DUSE_MYSQL=ON
  • Install: cmake --install build
  • Extension: .melt

Getting Started

Build

From the project root:

cmake -B build
cmake --build build

This produces build/melt. For MySQL support: cmake -B build -DUSE_MYSQL=ON.

Install (optional)

cmake --install build
cmake --install build --prefix $(HOME)/.local   # user-only

Run a program

./build/melt path/to/script.melt
./build/melt examples/hello.melt

First program

let name = "Melt";
print "Hello, ";
print name;

Paths: import paths are relative to the file containing the import. readFile(path) and similar resolve relative paths from the entry script’s directory. Run from the project root when using examples or the MVC app.

Command-line options

The melt executable accepts the following options. Options must appear before the script path.

melt [options] <file.melt>    Run a Melt script.
melt -e <code>                 Run inline code.
OptionDescription
-h, --helpShow usage and exit.
-v, --versionShow version and exit.
-c, --checkParse only (syntax check); do not run the script.
-e <code>Execute <code> as a Melt script (no file).
--traceTrace each executed statement to stderr (line number and statement kind).
--recursion-limit <N>Maximum call/execution depth; 0 = no limit (default).

Examples

./build/melt script.melt
./build/melt -c script.melt          # syntax check only
./build/melt -e 'print 1 + 2;'      # inline code
./build/melt --trace script.melt    # debug execution
./build/melt --recursion-limit 100 script.melt

How Melt works

These diagrams show how the interpreter runs a script and how loadable modules (extensions) are loaded.

Interpreter startup and execution

sequenceDiagram participant Main as main (binary) participant Interp as Interpreter participant Global as melt.ini participant Project as melt.config participant Loader as Module loader participant Ext as Extension .so/.dylib Main->>Interp: setBinDir(binDir) Main->>Interp: interpret(statements, entryPath) Interp->>Global: loadGlobalConfig() → binDir/melt.ini Interp->>Project: loadConfig(entryDir) → merge melt.config Interp->>Interp: registerBuiltins() Interp->>Loader: loadExtensions(binDir, extension_dir, extension list) Loader->>Ext: dlopen, melt_register(interp) Ext->>Interp: registerBuiltin(...) Interp->>Interp: execute statements

What the diagram means:

  • main (binary) — the melt executable. It resolves the directory containing the binary (binDir), creates the interpreter, and calls interpret() with the parsed script and entry file path.
  • Interpreter — holds config, module path, and built-in functions. It loads global config from binDir/melt.ini first, then merges project melt.config from the script’s directory.
  • registerBuiltins() — registers core built-ins (print, readFile, jsonDecode, etc.). Then, if extensions are enabled and listed in config, it calls the module loader.
  • Module loader — for each extension name in config, builds a path binDir/extension_dir/name.so (or .dylib/.dll), loads the shared library, finds the melt_register symbol, and calls it with the interpreter pointer.
  • Extension — the loaded library calls interp->registerBuiltin("name", fn) to add new built-in functions. After all extensions are loaded, the interpreter runs the script’s statements.

How loadable modules work

sequenceDiagram participant Interp as Interpreter participant Loader as Module loader participant Lib as Extension .so or .dylib Interp->>Loader: loadExtensions(binDir, extension_dir, extension list) Loader->>Loader: split list by comma loop For each extension name Loader->>Loader: path = binDir + extension_dir + name + suffix Loader->>Lib: dlopen(path) Loader->>Lib: dlsym melt_register Loader->>Lib: melt_register(interp) Lib->>Interp: registerBuiltin(name, fn) Note over Interp and Lib: Script can call new built-ins

What the diagram means:

  • Interpreter — reads extension_dir (default modules) and extension (e.g. example, math) from config. If extension_enabled is off, it skips loading.
  • Module loader — splits the extension list by comma, trims each name, and for each builds the full path: binDir + "/" + extension_dir + "/" + name + suffix (suffix is .so on Linux, .dylib on macOS, .dll on Windows).
  • dlopen — loads the shared library into the process. On failure (file missing or not a valid library), it logs to stderr and continues with the next extension.
  • melt_register(interp) — the extension must export extern "C" void melt_register(Interpreter* interp). The loader looks up this symbol and calls it once per library.
  • registerBuiltin — inside melt_register, the extension calls interp->registerBuiltin("name", function) for each built-in it provides. Those functions become available to the Melt script like any other built-in.

Language Reference

Comments

// line comment. /* ... */ block comment.

Statements & blocks

Statements end with ;. Blocks use { }.

Variables

let name = value; to declare. name = value; to reassign.

Types

Number, string, boolean (true, false), array [1,2,3], object (class instance). Strings are double-quoted; support \n \" etc.

Operators

Arithmetic: + - * / (plus string concatenation with +). Comparison: == != < <= > >=. Logical: && || ! (short-circuit for && and ||). Assignment: = for variables, obj.field = value, arr[i] = value.

Control flow

if (condition) { }
if (condition) { } else { }
for (init; condition; update) { }   // init: let i = 0 or expr; condition and update optional
foreach (value in arr) { }
foreach (i, value in arr) { }
foreach (key, value in obj) { }
while (condition) { }

for ( init ; condition ; update ) bodyinit runs once (optional: let i = 0 or any expression followed by ;). condition is checked each iteration (optional, empty means true). update runs after each body (optional; can be assignment, e.g. i = i + 1). foreach iterates arrays and objects; with one variable it binds value, with two it binds index/key and value.

Try / catch / throw

try {
    // statements that may throw
    throw "error message";
} catch (err) {
    // err holds the thrown value
    print err;

throw expr; evaluates expr and throws it; execution jumps to the nearest catch block. The catch variable (e.g. err) receives the thrown value (any type: string, number, etc.). Both Melt’s throw and interpreter runtime errors (e.g. unknown variable, “Expected array for index”) are caught; the catch variable gets the error message string for runtime errors. Parser/syntax errors still abort before run. return inside try is not caught and propagates out.

Print

print expr;

Classes & objects

class Name {
    method init(a, b) { this.x = a; this.y = b; }
    method getX() { return this.x; }
}
let obj = Name(1, 2);
obj.getX();  // returns 1

init is the constructor. this refers to the current object. return expr; or return; inside a method.

Arrays and map literal

Array: literal [a, b, c]; elements can be any type (numbers, strings, nested arrays, closures). Index: arr[i], arr[i] = value. Built-ins: arrayCreate() / arrayCreate(v1, v2, ...) (optional initial elements), arrayPush, arrayGet, arraySet, arrayLength.

Map (nested key-value) literal: [ "key" :=> value, ... ] — PHP-style; produces a JSON-like object. Use for nested structures (e.g. let store = [ "users" :=> [ [ "id" :=> 1, "name" :=> "Alice" ] ] ]). Keys are strings (or numbers/booleans, coerced to string). Access: store.users[0].name. See examples/store_nested_array.melt.

Import

import "path.melt";
import "path.melt" as M;

Path is relative to the importing file’s directory. One execution per resolved path. Without as: globals (variables, classes) from the file are shared in the current scope. With as M: the module runs in its own scope and its exports are bound to the single variable M (e.g. M.MyClass(1, 2)).

Truthiness

Falsy: false, 0, "", empty array, nothing. Everything else is truthy.

Keywords & literals

let if else for foreach in while print class method this import return try catch throw. Booleans: true false. Do not use method as a variable name.

Built-in Functions

Output

Built-inDescription
print expr;Prints value to stdout with newline.

Numbers

Formatting, random numbers, and math helpers.

FunctionDescription
numberFormat(n, decimals)String with n to decimals places (0–20).
random()Random number in [0, 1).
random(lo, hi)Random number in [lo, hi].
randomInt(lo, hi)Random integer in [lo, hi] inclusive.
round(n) floor(n) ceil(n) abs(n)Rounding and absolute value.
min(a, b) max(a, b)Smaller or larger of two numbers.
parseNumber(str)Parse string to number; returns 0 if invalid.

Modules and central config

Global: build/melt.ini (php.ini-style), read first. Project: melt.config next to entry script; merged after. Format: key = value, # comments. Keys: modulePath, extension_dir (default modules), extension (comma-separated names of loadable .so/.dylib/.dll). import "name" resolves: current dir, then each modulePath dir. Loadable extensions export melt_register(Interpreter*) and call interp->registerBuiltin(...).

FunctionDescription
addModulePath(dir)Add directory to module search path (relative to current script dir).
getConfig(key)Value from melt.ini / melt.config, or "".

Example: examples/module_config_demo/.

File I/O

FunctionDescription
readFile(path)Returns file contents as string, or "" on error.
writeFile(path, content)Writes string to file. Returns truthy/falsy.

Arrays

FunctionDescription
arrayCreate() / arrayCreate(v1, v2, ...)New array; with arguments, initializes with those elements (e.g. closures).
arrayPush(arr, value)Append; returns length.
arrayGet(arr, i)Element at index i.
arraySet(arr, i, value)Set element.
arrayLength(arr)Number of elements.

Vectors (math)

2D/3D vectors: vectorCreate2(x, y), vectorCreate3(x, y, z), vectorAdd, vectorSub, vectorScale, vectorLength, vectorDot, vectorCross (3D), vectorX/vectorY/vectorZ, vectorDim.

GUI render (image buffer)

imageCreate(w, h), imageFill(r, g, b), imageSetPixel(x, y, r, g, b), imageDrawLine(...), imageSavePpm(path), imagePreview() (window; requires default build (SDL2) and SDL2).

Strings

FunctionDescription
splitString(str, sep)Split by separator; returns array.
replaceString(str, from, to)Replace all occurrences.
escapeHtml(str)Escape for HTML output.
urlDecode(str)Decode %XX and + (e.g. form values).
chr(n)One-character string with ASCII code n (0–255).

JSON

FunctionDescription
jsonEncode(value)Value → JSON string.
jsonDecode(str)JSON string → value; objects support obj.field.
objectCreate()New empty object; use obj.field = value or obj[key] = value (string key), then jsonEncode(obj).
[ "key" :=> value, ... ]Map literal (nested key-value); same shape as objectCreate() + properties. Use :=> between key and value. Example: let o = [ "a" :=> 1, "b" :=> [ "x" :=> 2 ] ].

Dynamic keys: obj[key] and obj[key] = value when key is a string (e.g. variable). Missing key returns falsy.

Encoding / crypto

FunctionDescription
base64Encode(str)Base64 encode.
base64Decode(str)Base64 decode.
xorCipher(data, key)XOR with key (encrypt/decrypt; obfuscation only).

Views (templates)

FunctionDescription
renderView(templatePath, data)Loads HTML template; {{ key }} escaped, {!! key !!} raw. Returns rendered string.

HTTP server

Request: getRequestPath() getRequestMethod() getRequestBody() getRequestHeader(name)

Response: setResponseBody(str) setResponseStatus(code) setResponseContentType(str) setResponseHeader(name, value) streamChunk(str) servePublic(path)

Cookies: getCookie(name) — value of cookie from Cookie header, or "". setCookie(name, value [, path [, maxAge [, httpOnly]]]) — add Set-Cookie header (path default "/", maxAge default -1 omit, httpOnly 1 default).

Session: Server-side session keyed by melt_sid cookie (created automatically). sessionGet(key) — get string value; sessionSet(key, value) — set value (string, number, or boolean); sessionDestroy() — remove session and clear cookie.

Control: setHandler("ClassName") listen(port)

Multi-client: On Unix (macOS, Linux), the server forks a process per request so multiple clients are served concurrently (e.g. one user on a streaming page while others use the site). On Windows, a single worker handles requests one at a time (queued).

servePublic(path) serves from public/js, public/css, public/images when path matches.

HTTP client & upload

Outbound HTTP (curl-based):

FunctionDescription
httpRequest(method, url [, body [, headers]])Send HTTP request; returns object with body, status, error (if any). headers is optional string (e.g. "Content-Type: application/json").
httpGet(url)GET request; returns same object as httpRequest.
httpPost(url, body)POST request with body; returns same object.

Multipart upload (in request handler): When the request is multipart/form-data, the interpreter parses the body and exposes:

FunctionDescription
uploadFileName()Original filename of the uploaded file (first part), or "".
uploadFileData()Raw file data (string) of the first uploaded file.
uploadSave(path)Save the first uploaded file to path (relative to script dir). Returns truthy/falsy.

Stdio / MCP transport

For stdio-based servers (e.g. MCP): one message per line on stdin, one response per line on stdout. setMcpHandler("ClassName") then runMcp(). Handler’s handle() uses getMcpRequest(), jsonDecode/jsonEncode, setMcpResponse(str).

Architecture

The following diagram shows how the stdio transport works: the client sends one line per message on stdin; the runMcp() built-in sets the request, calls the Melt handler’s handle(), then writes the response line to stdout.

sequenceDiagram participant Client participant RunMcp as runMcp built-in participant Handler as Melt handle loop Per message Client->>RunMcp: line (stdin) RunMcp->>RunMcp: setMcpRequest(line) RunMcp->>Handler: callMcpHandler Handler->>Handler: getMcpRequest, jsonDecode, work, setMcpResponse RunMcp->>RunMcp: getMcpResponse RunMcp->>Client: response line (stdout) end

What the diagram means:

  • Client — the process that talks to your Melt script (e.g. an MCP host or another tool). It writes one line per message to the script’s stdin and reads one line per response from stdout.
  • runMcp built-in — the C++ code behind runMcp(). It runs a loop: read a line from stdin, store it as the “current request,” call your Melt handler, then send the handler’s response as one line on stdout.
  • Melt handle — your handler class’s handle() method. Inside it you call getMcpRequest() to get the line, parse it (e.g. jsonDecode), do your logic, and call setMcpResponse(str) to set the reply. That reply is what runMcp() sends back to the client.
  • The loop repeats for every message until stdin is closed (EOF), then runMcp() exits.
FunctionDescription
readStdinLine()Read one line from stdin; "" on EOF.
writeStdout(str)Write string to stdout (no newline); flushes.
writeStderr(str)Write string to stderr (no newline); for logging.
setMcpRequest(str) getMcpRequest()Set/get current MCP request line.
setMcpResponse(str) getMcpResponse()Set/get MCP response string.
setMcpHandler("ClassName")Set MCP handler class (must have handle()).
runMcp()Block: read line → call handler → write response line; exits when stdin closed.

MySQL (optional, -DUSE_MYSQL=ON)

FunctionDescription
mysqlConnect(host, user, password, database)Connect. Returns truthy/falsy.
mysqlQuery(sql)Execute SQL.
mysqlFetchRow()Next row as string; columns separated by chr(1). Split with splitString(row, chr(1)). Returns "" when no more rows.
mysqlFetchAll()All remaining rows as array of rows, e.g. [["1","Alice"],["2","Bob"]].
mysqlClose()Close connection.

SQLite (included in default build)

The default make builds with SQLite so sqliteOpen, sqliteExec, and the other SQLite built-ins are available without a separate target. You can still use default build (SQLite) for an explicit SQLite build.

FunctionDescription
sqliteOpen(path)Open/create SQLite database file. Returns truthy/falsy.
sqliteExec(sql)Execute SQL (CREATE/INSERT/UPDATE/DELETE).
sqliteQuery(sql)Prepare SELECT-style query for fetching rows.
sqliteFetchRow()Next row as string; columns separated by chr(1). Returns "" when done.
sqliteFetchAll()All remaining rows as array of rows, e.g. [["1","Alice"],["2","Bob"]].
sqliteClose()Close database and finalize active query.

QR code (optional, default build (QR))

Requires libqrencode. Build: default build (QR) (e.g. brew install qrencode on macOS).

FunctionDescription
qrGenerate(text [, cellSize])Encode text as a QR code; returns SVG string. Optional cellSize (1–32, default 4) is pixels per module. Use in HTML: {!! qrGenerate("https://example.com") !!} or serve with setResponseContentType("image/svg+xml").

Headless browser (extension, extension = headless_browser)

Requires Chrome, Chromium, or Edge. Build: CMake (see extensions/); enable in melt.config or melt.ini with extension = headless_browser.

FunctionDescription
headlessBrowserVersion()Extension version string.
browserAvailable()True if a supported browser was found on the system.
browserScreenshot(url, outputPath [, width [, height]])Open URL headless and save screenshot to outputPath (PNG). Optional viewport width/height.
browserDumpDom(url, outputPath)Open URL and dump DOM HTML to file.
browserPdf(url, outputPath)Open URL and print to PDF file.

Image optimize (extension, extension = image_optimize)

Uses sips (macOS) or ImageMagick when available. Build: CMake (see extensions/); enable with extension = image_optimize.

FunctionDescription
imageOptimizeVersion()Extension version string.
optimizeImage(inputPath, outputPath [, quality])Optimize/compress image (quality 1–100, default 80).
resizeImage(inputPath, outputPath, width, height)Resize image to given dimensions.

OS (extension, extension = os)

Build: CMake (see extensions/); enable with extension = os.

FunctionDescription
osVersion()Extension version string.
osName()OS name: windows, macos, linux, or unknown.
osArch()Architecture: x64, arm64, x86, or unknown.
osPwd()Current working directory path.
osGetEnv(key)Environment variable value, or "".
osExec(cmd)Run shell command; returns exit code (number).
osFileExists(path)True if path exists.
osMkdirs(path)Create directory and parents; returns truthy/falsy.

FFmpeg (extension, extension = ffmpeg)

Requires ffmpeg (and ffprobe for probe functions) on PATH. Build: CMake (see extensions/); enable with extension = ffmpeg.

FunctionDescription
ffmpegVersion()Extension version string.
ffmpegAvailable()True if ffmpeg is on PATH.
ffprobeAvailable()True if ffprobe is on PATH.
ffmpegConvert(inputPath, outputPath [, extraOptions])Transcode; optional extra args (e.g. "-b:v 1M"). Returns truthy on success.
ffmpegExtractAudio(inputPath, outputPath [, codec])Extract audio; codec: "mp3" (default), "aac", "opus".
ffmpegToGif(inputPath, outputPath [, width])Video to animated GIF; optional width (default 320).
ffprobeDuration(inputPath)Duration in seconds (number), or -1 on error.
ffprobeInfo(inputPath)JSON string with format and streams; use jsonDecode() in Melt.
ffmpegGenerateTestVideo(outputPath [, duration [, width [, height]]])Generate a short test video (lavfi testsrc). Defaults: 2 s, 320×240.

DateTime (extension, extension = datetime)

Date and time: timestamps, current date/time, format, parse, add, diff, components, timezone. Build: CMake (see extensions/); enable with extension = datetime. See extensions/datetime/README.md and examples/datetime_demo.melt.

FunctionDescription
dateVersion()Extension version string.
dateTimestamp()Current Unix timestamp (seconds).
dateTimestampMs()Current Unix timestamp (milliseconds).
dateNow()Current local date/time as YYYY-MM-DDTHH:MM:SS.
dateCurrentDate()Current date YYYY-MM-DD.
dateCurrentTime()Current time HH:MM:SS.
dateFormat(timestamp [, format])Format with strftime-style format (default %Y-%m-%d %H:%M:%S).
dateParse(dateString)Parse ISO-like YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS to Unix timestamp.
dateAdd(timestamp, amount, unit)Add time; unit: "second", "minute", "hour", "day", "month", "year".
dateDiff(timestamp1, timestamp2, unit)Difference (t2 − t1) in given unit.
dateYear(timestamp)dateSecond(timestamp)Extract year, month, day, hour, minute, second.
dateWeekday(timestamp)Day of week 0 (Sun)–6 (Sat).
dateStyle(timestamp, style)Predefined format: "short", "long", "datetime", "date", "time".
dateZoneOffset()Local UTC offset in hours (e.g. 5.5 for IST).
dateUtcOffsetString()Offset as string (e.g. +05:30, -08:00).

Zip (extension, extension = zip)

Create, extract, list, and read ZIP archives. Requires libzip (e.g. brew install libzip). Build: CMake (see extensions/); enable with extension = zip. See extensions/zip/README.md and examples/zip_demo.melt.

FunctionDescription
zipVersion()Extension version string.
zipCreate(zipPath, paths)Create ZIP at zipPath; paths = array of file paths to add.
zipExtract(zipPath, destDir)Extract all entries into destDir.
zipList(zipPath)Array of entry names (strings).
zipReadEntry(zipPath, entryName)Read one entry’s contents as string.
zipAddFiles(zipPath, paths)Add files to existing (or new) ZIP; paths = array of file paths.

HTTP Server & MVC Framework

Minimal server

class App {
    method handle() {
        let path = getRequestPath();
        if (path == "/") {
            setResponseBody("<h1>Hello</h1>");
            setResponseStatus(200);
        } else {
            setResponseStatus(404);
        }
    }
}
setHandler("App");
listen(8080);

Run: ./build/melt examples/server.melt then open http://localhost:8080/

MVC app (web_project_mvc)

Run: ./build/melt examples/web_project_mvc/main.melt

Structure: main.melt → config + routes; config/app.melt, config/database.melt; routes.melt (servePublic then controller); controllers/*.melt; models/; views/layout.melt + views/*.html (Blade-like {{ key }}, {!! key !!}); public/js, public/css, public/images; migrations/*.sql; run_migrations.melt.

Adding a route: Add branch in routes.melt, new controller file, render method in views/layout.melt, optional views/name.html.

Migrations: ./build/melt examples/web_project_mvc/run_migrations.melt. Add migrations/NNN_name.sql and append filename to migrationFiles in the runner.

Official website (official_website_using_melt)

Full Melt website: Home, Documentation, About, Resource, Support, and a dynamic blog (MySQL).

Run: ./build/melt examples/official_website_using_melt/main.melt then open http://localhost:4000

Structure: main.melt → config + routes; config/app.melt (port 4000), config/database.melt; routes.melt (servePublic then routes for /, /documentation, /about, /resource, /support, /blog, /blog/new, POST /blog, 404); controllers/*.melt (Home, Documentation, About, Resource, Support, Blog, NotFound); database/models/blog.melt (BlogPost); database/migrations/*.sql; database/seeders/*.melt; views/layout.melt + views/*.html; public/css/site.css; run_migrations.melt, run_seeders.melt.

Menu: Home (/), Documentation (/documentation), About (/about), Resource (/resource), Support (/support), Blog (/blog), New post (/blog/new).

Blog: All posts listed on /blog (drafts show “Draft” badge). Create at /blog/new; check “Publish” to show immediately. Edit config/database.melt for MySQL. Migrations: ./build/melt examples/official_website_using_melt/run_migrations.melt (requires -DUSE_MYSQL=ON).

Official Website (full reference)

The official Melt website lives in examples/official_website_using_melt/. It is built with Melt using the same MVC pattern as web_project_mvc and includes a dynamic blog backed by MySQL.

Run

./build/melt examples/official_website_using_melt/main.melt

Then open http://localhost:4000. Without MySQL, the site runs; the blog shows “No posts yet” and the New post form is available (creating a post requires MySQL).

Project structure

PathRole
main.meltEntry; setHandler, Router().dispatch(path), listen(app.port)
config/app.meltApp name, port (4000), tagline
config/database.meltMySQL: host, user, password, database
routes.meltservePublic then /, /documentation, /about, /resource, /support, /blog, /blog/new, POST /blog, 404
controllers/*.meltHome, Documentation, About, Resource, Support, Blog (index, newPost, createPost), NotFound
database/models/blog.meltBlogPost (id, title, body, published, created_at)
views/layout.meltView: renderHome, renderDocumentation, renderAbout, renderResource, renderSupport, renderBlog(posts), renderBlogNew, renderBlogCreated, renderNotFound
views/*.htmllayout, home, documentation, about, resource, support, blog, blog_new, blog_created, 404
public/css/site.cssStyles
database/migrations/*.sqlblog_posts table
database/seeders/*.meltSeed sample data
run_migrations.meltRun database/migrations (with-mysql)
run_seeders.meltRun database/seeders

Menu and routes

MenuPath
Home/
Documentation/documentation
About/about
Resource/resource
Support/support
Blog/blog
New post/blog/new

Blog and database

Table: blog_posts (id, title, body, published, created_at). All posts shown on /blog; drafts show a “Draft” badge.

Migrations: -DUSE_MYSQL=ON, then ./build/melt examples/official_website_using_melt/run_migrations.melt. Edit config/database.melt for your MySQL.

Create post: Open /blog/new, fill title and body, optionally check “Publish”, submit.

Development guide

Building the interpreter and writing loadable extensions.

Building the interpreter

From the repo root:

cmake -B build && cmake --build build   # → build/melt
# Add targets in CMakeLists.txt for extensions → bin/modules/

Optional: -DUSE_MYSQL=ON. Config is read from build/melt.ini and project melt.config.

Extension contract

Loadable extensions are shared libraries. Each must export:

extern "C" void melt_register(Interpreter* interp);

Melt calls melt_register(interp) after loading. Register built-ins with:

interp->registerBuiltin("functionName", yourNativeFn);

Built-in function signature

Type Interpreter::NativeFn:

Value yourBuiltin(Interpreter* interp, std::vector<Value> args);

Value is a std::variant of std::monostate, double, std::string, bool, and object/array/class types. Return e.g. Value(3.14) or Value(std::string("hi")).

Include path

Extensions need the same includes as the main binary so #include "interpreter.hpp" works (e.g. -I src -I src/core from repo root).

Building extensions

  • Example: CMake (see extensions/) builds extensions/example/example.cpp into bin/modules/example.dylib (macOS), example.so (Linux), or example.dll (Windows).
  • macOS: use -fPIC -shared -undefined dynamic_lookup so the extension resolves Interpreter::registerBuiltin from the main binary at load time.
  • Linux: main binary is built with -rdynamic so loaded .so files can resolve interpreter symbols.

Configuration (melt.ini)

In build/melt.ini or project melt.config:

  • extension_enabled = 1|0|true|false|on|off — enable/disable loading extensions (default: on).
  • extension_dir = directory relative to the melt binary (default: modules).
  • extension = comma-separated extension names to load (e.g. example, math).

Libraries are loaded from <binDir>/<extension_dir>/<name>.so|.dylib|.dll.

Example extension

See extensions/example/example.cpp for a minimal extension (exampleVersion, exampleAdd, exampleSqrt). Example docs: extensions/example/index.html.

Adding your own extension

  1. Add a folder under extensions/ with a .cpp that implements melt_register and registerBuiltin calls.
  2. Add a CMake target (or Makefile wrapper target) to build a shared library into bin/modules/<name>.so|.dylib|.dll.
  3. Set extension = <name> (or append to the list) in melt.ini.

Module development guide (step by step)

This guide explains how to create a Melt loadable module (extension) in simple steps. A module is a shared library (.so, .dylib, or .dll) that adds new built-in functions to Melt. Your Melt scripts can then call these functions like any other built-in.

What you need

  • A C++ compiler (same as for building Melt: g++ or clang++).
  • The Melt source code (so you can #include "interpreter.hpp").
  • One or more .cpp files that implement your functions and the melt_register entry point.

Step 1: Create your module folder and source file

Create a folder under extensions/, for example extensions/mymodule/. Inside it, create a .cpp file (e.g. mymodule.cpp). This file will contain all your native functions and the registration function.

Step 2: Include the interpreter header

At the top of your .cpp file, include the Melt interpreter so you can use Interpreter, Value, and registerBuiltin:

#include "interpreter.hpp"

When building, you must pass the same include paths as the main Melt binary (e.g. -I src -I src/core from the repo root).

Step 3: Write a built-in function

Every built-in has the same C++ signature:

static Value myFunc(Interpreter* interp, std::vector<Value> args) {
    // interp: use if you need the interpreter (e.g. get variables)
    // args: the arguments passed from Melt (e.g. myFunc(1, 2) → args[0]=1, args[1]=2)
    // Return a Value: number, string, bool, etc.
    return Value(42.0);           // number
    return Value(std::string("hi"));  // string
    return Value(true);          // boolean
}

Reading arguments: args is a vector of Value. Each Value can hold a number (double), string, bool, array, or object. Use std::get_if<double>(&args[0]) to get a number, std::get_if<std::string>(&args[0]) for a string, and so on. Check args.size() before indexing.

Returning: Return Value(...). For "no value" you can return Value() (monostate).

Step 4: Implement the registration entry point

Melt loads your library and looks for a single function named melt_register. It must be extern "C" so the name is not mangled. Inside it, register each of your built-ins with the interpreter:

extern "C" void melt_register(Interpreter* interp) {
    interp->registerBuiltin("myFunc", myFunc);
    interp->registerBuiltin("myOtherFunc", myOtherFunc);
}

The first argument is the name as seen in Melt (your script will call myFunc(...)). The second is the C++ function pointer. Melt calls melt_register(interp) once when the extension is loaded.

Step 5: Build the shared library

You must compile your .cpp into a shared library and put it where Melt can find it.

  • Linux: g++ -std=c++17 -fPIC -shared -I src -I src/core -o bin/modules/mymodule.so extensions/mymodule/mymodule.cpp (Melt's main binary is built with -rdynamic so the .so can resolve interpreter symbols.)
  • macOS: clang++ -std=c++17 -fPIC -shared -undefined dynamic_lookup -I src -I src/core -o bin/modules/mymodule.dylib extensions/mymodule/mymodule.cpp
  • Windows: Build a .dll with similar flags and place it in bin/modules/mymodule.dll.

Or add a target to CMakeLists.txt (or the Makefile wrapper) to build your library into bin/modules/.

Step 6: Enable the extension in config

Create or edit build/melt.ini (next to the melt binary). Add or set:

extension_dir = modules
extension = example,mymodule

extension_dir is the folder under the binary directory where Melt looks for libraries (default modules). extension is a comma-separated list of extension names (no suffix). Melt will load bin/modules/mymodule.so (or .dylib / .dll) and call melt_register.

Step 7: Use your functions in Melt

Run a script that calls your new built-ins. For example:

let x = myFunc(1, 2);
print myOtherFunc("hello");

If the extension failed to load, Melt will print an error to stderr. If it loaded, your functions are available globally like print or readFile.

Summary

  1. Create extensions/<name>/<name>.cpp.
  2. Include interpreter.hpp.
  3. Implement one or more functions with signature Value fn(Interpreter*, std::vector<Value> args).
  4. Implement extern "C" void melt_register(Interpreter* interp) and call interp->registerBuiltin("name", fn) for each.
  5. Build a shared library into bin/modules/<name>.so|.dylib|.dll.
  6. Set extension = <name> in build/melt.ini (or project melt.config).
  7. Run ./build/melt your_script.melt and call your built-ins from the script.

See extensions/example/example.cpp for a minimal working example (exampleVersion, exampleAdd, exampleSqrt).

Embedded guide

This section explains the embedded build of Melt, when to use it, what it includes and excludes, and how to run a sample embedded-style application.

What is the embedded build?

The embedded build (embedded build) produces a smaller binary, build/melt-embedded, by excluding optional subsystems that are not needed on resource-constrained or embedded-style targets:

  • HTTP server — no listen(), setHandler(), getRequestPath(), servePublic(), etc. Scripts that call these get a clear runtime error: "Melt embedded build: HTTP server not available".
  • MySQL / SQLite — no mysqlConnect, mysqlQuery, mysqlFetchRow, mysqlFetchAll, mysqlClose, sqliteOpen, sqliteExec, sqliteQuery, sqliteFetchRow, sqliteFetchAll, sqliteClose. Those built-ins are not registered (calling them yields "unknown variable").
  • GUI (SDL2) — no imagePreview(). The GUI window is not linked. Scripts that call it get a runtime error.

The embedded binary still includes the core language (variables, types, control flow, classes, methods, import, try/catch/throw) and built-ins such as print, readFile, writeFile, arrays, JSON, base64, xorCipher, vectors, and the module loader (loadable extensions). So you can script logic, file I/O, and optional native extensions (e.g. GPIO or UART) without pulling in HTTP, MySQL, or SDL2.

When to use it

  • Smaller binary and fewer dependencies — Use melt-embedded when you run Melt on a device (e.g. Raspberry Pi, BeagleBone) or in an environment where you do not need a web server, database client, or GUI. The binary is smaller and does not link libmysqlclient or SDL2.
  • Embedded-style scripting — Use it for scripts that implement control logic, read/write files or device nodes, or call into native extensions that talk to hardware (GPIO, UART, etc.). The script runs in a loop or is invoked by a host process; it does not start an HTTP server.
  • CI or minimal installs — Use the embedded build in CI or minimal installs where you only need to run Melt scripts that do not use HTTP, MySQL, or GUI.

What is included vs excluded

FeatureDefault meltmelt-embedded
Core language (syntax, classes, import)YesYes
print, readFile, writeFile, arrays, JSON, base64, xorCipher, vectorsYesYes
Loadable extensions (module_loader)YesYes
HTTP server (listen, setHandler, servePublic, …)YesNo (stub throws)
MySQL built-insOnly if -DUSE_MYSQL=ONNo
SQLite built-insYes (default build)No
GUI (imagePreview)Only if default build (SDL2)No (stub throws)

How to build and run

embedded build

This produces build/melt-embedded. Run scripts with:

./build/melt-embedded path/to/script.melt

Use the same working directory and path rules as for melt (e.g. run from project root when using import or relative paths in readFile). Do not call listen(), MySQL built-ins, or imagePreview() in scripts intended for the embedded binary.

Sample embedded system

A sample embedded-style application is provided in examples/embedded_system/. It demonstrates a simple control loop: read "sensor" input (from a file or simulated value), update state, and write "actuator" output (to a file or stdout). This pattern is typical of embedded or IoT scripts that run on a small device.

  • examples/embedded_system/main.melt — Entry point: loads config, runs the main loop for a number of cycles, and exits.
  • examples/embedded_system/config.melt — Configuration (e.g. cycle count, paths for sensor/actuator files).
  • examples/embedded_system/loop.melt — Core logic: read sensor, compute state, write output. Imported by main.

Run it with the embedded binary (from project root):

./build/melt-embedded examples/embedded_system/main.melt

You can use the full build/melt as well; the script does not use HTTP, MySQL, or GUI. The embedded build is recommended so the binary and dependencies stay minimal.

Extending for real hardware

On a real embedded or single-board target (e.g. Raspberry Pi running Linux), you can:

  1. Build melt-embedded on the device or cross-compile, and install it (e.g. cmake --install build --prefix /usr/local (or extend the build for an embedded binary)).
  2. Implement a loadable extension that provides built-ins such as gpioRead(pin), gpioWrite(pin, value), uartWrite(str), or delayMs(ms). Register them in melt_register and load the extension via melt.ini or melt.config.
  3. Write Melt scripts that call those built-ins in a loop, or invoke the script from a systemd unit or cron job. The sample in examples/embedded_system/ uses file I/O as a stand-in for hardware; replace that with calls to your extension once it is available.

Summary

  • Build: embedded buildbuild/melt-embedded.
  • Use when: You need a smaller binary and do not need HTTP, MySQL, or GUI.
  • Run: ./build/melt-embedded script.melt. Avoid listen(), MySQL, and imagePreview().
  • Sample: examples/embedded_system/main.melt shows a simple sensor–logic–actuator loop; run it with melt-embedded.

Examples

Run from project root: ./build/melt examples/<path>.melt

FileDescription
hello.meltMinimal print.
basics.meltVariables, conditionals, loops.
oop.meltClasses, methods, this.
counter.meltClass with state.
return_demo.meltMethod return values.
try_catch_demo.melttry/catch and throw.
try_catch_runtime_error.meltCatching interpreter runtime errors.
logical_ops_demo.meltLogical operators &&, ||, !.
comment_demo.meltComments.
multi_file/main.meltImport and use other .melt files.
file_io.meltreadFile, writeFile.
embedded_system/main.meltSample embedded-style loop (sensor–logic–actuator); use with ./build/melt-embedded.
array_demo.meltArrays and built-ins.
array_closures_demo.meltArrays with closures (literal and arrayCreate), callbacks.
array_create_with_closures.meltarrayCreate with initial elements (e.g. lambdas).
import_as_demo.meltimport "path.melt" as M; — bind module to one variable, use M.MyClass.
lambda_demo.meltFirst-class functions, fn(a, b) { ... }, closures.
object_create_demo.meltobjectCreate(), obj[key], obj[key] = value, jsonEncode.
php_array_to_melt.meltPHP-style associative array → Melt objects (objectCreate, nested structure).
foreach_demo.meltforeach over arrays, objects, and SQL row arrays.
json_demo.meltjsonEncode, jsonDecode.
encryption_demo.meltBase64, xorCipher.
server.meltHTTP server.
session_cookies_demo.meltSession and cookies: getCookie, setCookie, sessionGet, sessionSet, sessionDestroy. Port 8765.
mysql_example.meltMySQL (requires with-mysql).
sqlite_example.meltSQLite (included in default build).
qr_demo.meltQR code demo server: form to generate QR for any text (requires default build (QR)). Port 8766.
upload_server.meltHTTP server with multipart file upload (uploadSave, uploadFileName, uploadFileData).
rest_api_client.meltOutbound HTTP: httpGet, httpPost, httpRequest.
image_optimize_demo.meltImage optimize extension: optimizeImage, resizeImage (requires extension = image_optimize).
os_extension_demo.meltOS extension: osName, osPwd, osGetEnv, osExec, osFileExists, osMkdirs (requires extension = os).
headless_browser_demo.meltHeadless browser extension: screenshot, dump DOM, PDF (requires extension = headless_browser, Chrome/Chromium/Edge).
ffmpeg_demo.meltFFmpeg extension: convert, extract audio, to GIF, ffprobe duration/info (requires extension = ffmpeg, ffmpeg/ffprobe on PATH).
web_project_mvc/main.meltFull MVC app.
official_website_using_melt/main.meltOfficial Melt website (MVC): Home, Documentation, About, Resource, Support, Blog. Port 4000.

Melt — minimal OOP language. Back to docs index