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 build→build/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.
| Option | Description |
|---|---|
-h, --help | Show usage and exit. |
-v, --version | Show version and exit. |
-c, --check | Parse only (syntax check); do not run the script. |
-e <code> | Execute <code> as a Melt script (no file). |
--trace | Trace 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
What the diagram means:
- main (binary) — the
meltexecutable. It resolves the directory containing the binary (binDir), creates the interpreter, and callsinterpret()with the parsed script and entry file path. - Interpreter — holds config, module path, and built-in functions. It loads global config from
binDir/melt.inifirst, then merges projectmelt.configfrom 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 themelt_registersymbol, 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
What the diagram means:
- Interpreter — reads
extension_dir(defaultmodules) andextension(e.g.example, math) from config. Ifextension_enabledis 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.soon Linux,.dylibon macOS,.dllon 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 callsinterp->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 ) body — init 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 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-in | Description |
|---|---|
print expr; | Prints value to stdout with newline. |
Numbers
Formatting, random numbers, and math helpers.
| Function | Description |
|---|---|
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(...).
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
readFile(path) | Returns file contents as string, or "" on error. |
writeFile(path, content) | Writes string to file. Returns truthy/falsy. |
Arrays
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
base64Encode(str) | Base64 encode. |
base64Decode(str) | Base64 decode. |
xorCipher(data, key) | XOR with key (encrypt/decrypt; obfuscation only). |
Views (templates)
| Function | Description |
|---|---|
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):
| Function | Description |
|---|---|
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:
| Function | Description |
|---|---|
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.
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 callgetMcpRequest()to get the line, parse it (e.g.jsonDecode), do your logic, and callsetMcpResponse(str)to set the reply. That reply is whatrunMcp()sends back to the client. - The loop repeats for every message until stdin is closed (EOF), then
runMcp()exits.
| Function | Description |
|---|---|
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)
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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).
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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.
| Function | Description |
|---|---|
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
| Path | Role |
|---|---|
| main.melt | Entry; setHandler, Router().dispatch(path), listen(app.port) |
| config/app.melt | App name, port (4000), tagline |
| config/database.melt | MySQL: host, user, password, database |
| routes.melt | servePublic then /, /documentation, /about, /resource, /support, /blog, /blog/new, POST /blog, 404 |
| controllers/*.melt | Home, Documentation, About, Resource, Support, Blog (index, newPost, createPost), NotFound |
| database/models/blog.melt | BlogPost (id, title, body, published, created_at) |
| views/layout.melt | View: renderHome, renderDocumentation, renderAbout, renderResource, renderSupport, renderBlog(posts), renderBlogNew, renderBlogCreated, renderNotFound |
| views/*.html | layout, home, documentation, about, resource, support, blog, blog_new, blog_created, 404 |
| public/css/site.css | Styles |
| database/migrations/*.sql | blog_posts table |
| database/seeders/*.melt | Seed sample data |
| run_migrations.melt | Run database/migrations (with-mysql) |
| run_seeders.melt | Run database/seeders |
Menu and routes
| Menu | Path |
|---|---|
| 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/)buildsextensions/example/example.cppintobin/modules/example.dylib(macOS),example.so(Linux), orexample.dll(Windows). - macOS: use
-fPIC -shared -undefined dynamic_lookupso the extension resolvesInterpreter::registerBuiltinfrom the main binary at load time. - Linux: main binary is built with
-rdynamicso loaded.sofiles 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
- Add a folder under
extensions/with a.cppthat implementsmelt_registerandregisterBuiltincalls. - Add a CMake target (or Makefile wrapper target) to build a shared library into
bin/modules/<name>.so|.dylib|.dll. - Set
extension = <name>(or append to the list) inmelt.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_registerentry 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-rdynamicso 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
- Create
extensions/<name>/<name>.cpp. - Include
interpreter.hpp. - Implement one or more functions with signature
Value fn(Interpreter*, std::vector<Value> args). - Implement
extern "C" void melt_register(Interpreter* interp)and callinterp->registerBuiltin("name", fn)for each. - Build a shared library into
bin/modules/<name>.so|.dylib|.dll. - Set
extension = <name>inbuild/melt.ini(or projectmelt.config). - Run
./build/melt your_script.meltand 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-embeddedwhen 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
| Feature | Default melt | melt-embedded |
|---|---|---|
| Core language (syntax, classes, import) | Yes | Yes |
| print, readFile, writeFile, arrays, JSON, base64, xorCipher, vectors | Yes | Yes |
| Loadable extensions (module_loader) | Yes | Yes |
| HTTP server (listen, setHandler, servePublic, …) | Yes | No (stub throws) |
| MySQL built-ins | Only if -DUSE_MYSQL=ON | No |
| SQLite built-ins | Yes (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:
- Build
melt-embeddedon the device or cross-compile, and install it (e.g.cmake --install build --prefix /usr/local(or extend the build for an embedded binary)). - Implement a loadable extension that provides built-ins such as
gpioRead(pin),gpioWrite(pin, value),uartWrite(str), ordelayMs(ms). Register them inmelt_registerand load the extension viamelt.iniormelt.config. - 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 build→build/melt-embedded. - Use when: You need a smaller binary and do not need HTTP, MySQL, or GUI.
- Run:
./build/melt-embedded script.melt. Avoidlisten(), MySQL, andimagePreview(). - Sample:
examples/embedded_system/main.meltshows a simple sensor–logic–actuator loop; run it withmelt-embedded.
Examples
Run from project root: ./build/melt examples/<path>.melt
| File | Description |
|---|---|
| hello.melt | Minimal print. |
| basics.melt | Variables, conditionals, loops. |
| oop.melt | Classes, methods, this. |
| counter.melt | Class with state. |
| return_demo.melt | Method return values. |
| try_catch_demo.melt | try/catch and throw. |
| try_catch_runtime_error.melt | Catching interpreter runtime errors. |
| logical_ops_demo.melt | Logical operators &&, ||, !. |
| comment_demo.melt | Comments. |
| multi_file/main.melt | Import and use other .melt files. |
| file_io.melt | readFile, writeFile. |
| embedded_system/main.melt | Sample embedded-style loop (sensor–logic–actuator); use with ./build/melt-embedded. |
| array_demo.melt | Arrays and built-ins. |
| array_closures_demo.melt | Arrays with closures (literal and arrayCreate), callbacks. |
| array_create_with_closures.melt | arrayCreate with initial elements (e.g. lambdas). |
| import_as_demo.melt | import "path.melt" as M; — bind module to one variable, use M.MyClass. |
| lambda_demo.melt | First-class functions, fn(a, b) { ... }, closures. |
| object_create_demo.melt | objectCreate(), obj[key], obj[key] = value, jsonEncode. |
| php_array_to_melt.melt | PHP-style associative array → Melt objects (objectCreate, nested structure). |
| foreach_demo.melt | foreach over arrays, objects, and SQL row arrays. |
| json_demo.melt | jsonEncode, jsonDecode. |
| encryption_demo.melt | Base64, xorCipher. |
| server.melt | HTTP server. |
| session_cookies_demo.melt | Session and cookies: getCookie, setCookie, sessionGet, sessionSet, sessionDestroy. Port 8765. |
| mysql_example.melt | MySQL (requires with-mysql). |
| sqlite_example.melt | SQLite (included in default build). |
| qr_demo.melt | QR code demo server: form to generate QR for any text (requires default build (QR)). Port 8766. |
| upload_server.melt | HTTP server with multipart file upload (uploadSave, uploadFileName, uploadFileData). |
| rest_api_client.melt | Outbound HTTP: httpGet, httpPost, httpRequest. |
| image_optimize_demo.melt | Image optimize extension: optimizeImage, resizeImage (requires extension = image_optimize). |
| os_extension_demo.melt | OS extension: osName, osPwd, osGetEnv, osExec, osFileExists, osMkdirs (requires extension = os). |
| headless_browser_demo.melt | Headless browser extension: screenshot, dump DOM, PDF (requires extension = headless_browser, Chrome/Chromium/Edge). |
| ffmpeg_demo.melt | FFmpeg extension: convert, extract audio, to GIF, ffprobe duration/info (requires extension = ffmpeg, ffmpeg/ffprobe on PATH). |
| web_project_mvc/main.melt | Full MVC app. |
| official_website_using_melt/main.melt | Official Melt website (MVC): Home, Documentation, About, Resource, Support, Blog. Port 4000. |
Melt — minimal OOP language. Back to docs index