A lightweight embeddable scripting language written in pure C.
Current version: 1.0.0
Just is a small scripting language built from scratch in C.
- Single binary — no runtime, no package manager. Optional libraries (SQLite, HTTPS) are baked in at link time, not loaded from separate DLLs/.so files at runtime
- Cross-platform — Windows and Linux from the same code
- Familiar syntax — if you know JavaScript or Python, you'll feel at home
- Tracing GC — mark-and-sweep garbage collector with a configurable threshold
- Thread-safe — a single interpreter instance can be called from multiple threads safely (calls are serialized internally); separate instances run fully in parallel
- Modules — split code across files with
import - C plugins — extend the language with native code (graphics/audio, custom I/O, anything)
- Structured error handling —
try/catch/finally/throw, with catchable runtime errors instead of crashes (division by zero, stack overflow, etc.) - Closures & lambdas —
func(x) { ... }expressions with capture-by-value - Capability-based sandboxing — turn off file, network, exec, plugin, or database access per-interpreter
- Resource limits — cap iterations, call depth, and token count to safely run untrusted scripts
- Regex — built in, always available, zero extra setup
- 100+ builtins — JSON, HTTP(S), files, strings, math, collections, regex, SQLite, and more
- SQLite built-in — full database support out of the box (optional at build time)
- HTTPS built-in — TLS-encrypted requests via mbedTLS (optional at build time)
It compiles in seconds and runs anywhere. Good for automation scripts, configuration with logic, learning how languages work, or embedding into larger C/C++ projects — including ones that need to run scripts from untrusted sources, or call into the same interpreter from multiple threads.
Windows (MinGW), with the interactive build script:
build.bat
It asks whether to include SQLite and/or HTTPS and produces just.exe either way. See Building below for what files need to be present for each.
One command, no script, any platform — see Building for the full breakdown of flags.
just hello.just
just
hello.just:
print("Hello from Just!")// Variables
let name = "Just"
const version = 1
// Objects
let user = { name: "Alex", age: 25 }
// Arrays (support negative indices)
let items = [1, 2, 3]
print(items[-1]) // 3
// Functions
func greet(name) {
return "Hello, " + name
}
// Closures / lambdas
let square = func(x) { return x * x }
print(square(5)) // 25
// Logical operators — both keyword and symbol forms work
if (user.age >= 18 and user.age < 65) {
print("Adult")
}
if (user.age >= 18 && !false) {
print("Also adult")
}
// Loops
for (let i = 0; i < 5; i = i + 1) {
print(i)
}🤖 For LLMs/AI assistants: See
LLM-reference.md— a concise technical reference optimized for AI code generation.
Runtime failures that used to crash the interpreter now raise catchable errors: division/modulo by zero, exceeding the call-depth limit, and explicit error()/throw calls.
try {
let x = 5 / 0
} catch (e) {
print("Caught:", e)
}
// throw supports both strings and structured objects
try {
throw ({ code: 404, message: "not found" })
} catch (e) {
print(e.code, e.message) // 404 not found
}
// finally always runs, even when the try block returns
func withCleanup() {
try {
return "ok"
} finally {
print("cleanup ran")
}
}
withCleanup()Anonymous functions capture the enclosing scope by value at the moment they're created:
func make_adder(n) {
return func(x) { return x + n }
}
let add5 = make_adder(5)
let add10 = make_adder(10)
print(add5(1)) // 6
print(add10(1)) // 11Lambdas work anywhere a value is expected — as arguments to filter, map, reduce, stored in variables, or called directly. Because capture is by value, a lambda assigned to a let can't call itself recursively — use a named func for recursion instead.
Built directly into the core (via tiny-regex-c, public domain) — no build flag, no setup, always available.
print(regex_match("hello world", "world")) // true
print(regex_find("id=42", "\\d+")) // "42"
print(regex_find_all("a1 b22 c333", "\\d+")) // ["1", "22", "333"]
print(regex_replace("x1y2z3", "\\d", "#")) // "x#y#z#"Supports: . ^ $ * + ? [abc] [^abc] [a-z] \s \S \w \W \d \D. Does not support capture groups, alternation (|), or {n,m} repetition — it's intentionally minimal, good for validation/extraction, not a full PCRE replacement. Like any naive backtracking regex engine, avoid running untrusted patterns against untrusted, attacker-sized input.
Split your code across multiple files with import:
math.just:
func add(a, b) {
return a + b
}
func multiply(a, b) {
return a * b
}main.just:
import "math.just"
print(add(10, 20)) // 30
print(multiply(5, 6)) // 30Imports are loaded once and share the global scope. Good for organizing larger projects.
| Category | Functions |
|---|---|
| Output | print |
| Types | type, int, str, bool, is_number, is_string, is_bool, is_array, is_object, is_function, is_null |
| Input | input |
| Strings | upper, lower, trim, split, join, replace, contains, len, starts_with, ends_with, repeat, substr, pad_start, pad_end |
| Regex | regex_match, regex_find, regex_find_all, regex_replace |
| Math | sqrt, pow, abs, min, max, floor, ceil, round, random, sin, cos, tan, log, exp, clamp |
| Constants | PI, E |
| Paths | dirname, basename, extname, join_path |
| JSON | json, json_parse, read_json, write_json, export, import_json |
| HTTP(S) | http_get, http_post (both transparently support http:// and https:// URLs) |
| Files | read, write, exists |
| Time | now, sleep |
| System | exec, env, load_plugin |
| Modules | import |
| Collections | len, range, filter, map, reduce, find, keys, values, entries, has, merge, array_push, array_pop, first, last, reverse, sort, slice, concat, unique, sum, index_of, includes |
| Colors | red, green, blue, yellow, magenta, cyan, bold |
| Debug | debug, dump, error, help, task, watch, gc_get_count, gc_get_allocations |
| SQLite | db_open, db_close, db_query, db_exec, db_prepare, db_bind, db_step, db_finalize, db_last_insert_id, db_changes, db_begin, db_commit, db_rollback, db_error |
Just can restrict what a script is allowed to do. This is useful when running scripts you don't fully trust (plugins, user-submitted automation, config files with logic, etc.) via the C API:
#include "just.h"
// Only allow pure computation — no files, network, exec, plugins, or DB.
// This is also a good fit for "config file with logic" use cases: the
// script can compute values but can't touch the outside world.
JustState *j = just_init_ex(0);
// Or pick specific capabilities:
JustState *j2 = just_init_ex(JUST_CAP_FILES | JUST_CAP_NET);
// just_init() is unchanged and grants everything:
JustState *full = just_init(); // == just_init_ex(JUST_CAP_ALL)| Flag | Gates |
|---|---|
JUST_CAP_EXEC |
exec() |
JUST_CAP_FILES |
read, write, exists, read_json, write_json |
JUST_CAP_NET |
http_get, http_post |
JUST_CAP_PLUGIN |
load_plugin() |
JUST_CAP_DB |
all db_* functions |
JUST_CAP_ALL |
everything (default via just_init()) |
A script that tries a disabled capability gets a catchable runtime error rather than crashing or silently succeeding. Regex has no capability flag — it's always available since it can't reach outside the interpreter.
Also aimed at running untrusted or long scripts safely:
just_set_max_iterations(j, 1000000); // cap on loop iterations
just_set_max_call_depth(j, 100); // cap on recursion depth
just_set_max_tokens(j, 500000); // cap on parsed token count
just_gc_set_threshold(j, 20000); // allocations before an auto GC pass
int live = just_gc_get_count(j); // currently live GC-tracked values
int total = just_gc_get_allocations(j);// allocations since last collectionDefaults (overridable at compile time via #define before including just.h, or at runtime via the setters above): MAX_ITERATIONS, MAX_CALL_DEPTH (150), MAX_TOKENS, GC_THRESHOLD (50000), among others — see just.h.
A single JustState instance can now be shared across threads safely — calls to just_eval, just_eval_file, just_call, and the variable/GC/registration API are internally serialized (similar in spirit to Python's GIL: queued, not truly parallel, but never racing or corrupting state). Separate JustState instances remain fully independent and run in true parallel on separate threads with no shared locking at all.
JustState *shared = just_init();
// safe to call just_eval(shared, ...) / just_call(shared, ...) from multiple
// worker threads concurrently — they'll be serialized, not corruptedFor host applications that call the same Just function repeatedly — once per frame in a game loop, once per request in a server — just_call invokes an already-defined function directly without re-tokenizing a call-expression string through just_eval() every time:
just_eval(j, "func update(dt) { ... }"); // define once
// then, every frame:
Value *args[1] = { just_number(delta_time) };
Value *result = just_call(j, "update", args, 1);http_get/http_post transparently support https:// URLs when built with mbedTLS (see Building). Without it (-DJUST_NO_TLS), https:// calls return an empty string with a clear error message — http:// always works either way.
Known limitation (v1): certificate verification is disabled — there's no cross-platform way to reliably locate a trusted CA bundle (especially on older Windows versions), so this is deferred rather than shipped half-working. Connections are encrypted (safe from passive eavesdropping) but not verified against a certificate authority (not safe from an active man-in-the-middle). Treat it accordingly — fine for calling APIs over your own network/VPN or trusted infrastructure, not yet for handling untrusted/adversarial network paths.
Just can bundle SQLite directly in the binary — no external libraries needed. It's opt-in at build time: drop sqlite3.c/sqlite3.h next to just.c and build normally (or answer "yes" in build.bat). Build with -DJUST_NO_SQLITE to leave it out entirely.
let db = db_open("app.db")
// Create table
db_query(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT, age INTEGER)")
// Insert data with transaction
db_begin(db)
db_query(db, "INSERT INTO users VALUES (1, 'Alex', 25)")
db_query(db, "INSERT INTO users VALUES (2, 'Bob', 30)")
db_commit(db)
// Query data
let users = db_query(db, "SELECT * FROM users")
print(users) // [{id: 1, name: Alex, age: 25}, ...]
// Safe prepared statements (protects against SQL injection)
let stmt = db_prepare(db, "SELECT * FROM users WHERE age > ?")
db_bind(stmt, 1, 25)
while true {
let row = db_step(stmt)
if row == null { break }
print(row.name, "is", row.age, "years old")
}
db_finalize(stmt)
// Get last inserted ID
let id = db_last_insert_id(db)
print("Last ID:", id)
db_close(db)SQLite Functions:
| Function | Description |
|---|---|
db_open(filename) |
Open or create database |
db_close(db) |
Close database |
db_query(db, sql) |
Execute SQL (returns array for SELECT) |
db_exec(db, sql) |
Alias for db_query |
db_prepare(db, sql) |
Prepare statement |
db_bind(stmt, idx, val) |
Bind value (NULL, number, string, bool) |
db_step(stmt) |
Fetch next row (returns object or null) |
db_finalize(stmt) |
Finalize statement |
db_last_insert_id(db) |
Last auto-increment ID |
db_changes(db) |
Number of changed rows |
db_begin(db) |
Start transaction |
db_commit(db) |
Commit transaction |
db_rollback(db) |
Rollback transaction |
db_error(db) |
Last error message |
Gated by JUST_CAP_DB — see Security & Sandboxing.
Just is designed to be embedded in C/C++ projects:
#include "just.h"
JustState *j = just_init(); // or just_init_ex(capabilities) for a sandboxed instance
// Register C functions
just_register_function(j, "c_hello", c_hello);
// Set variables
Value *player = just_object_new(j);
just_object_set(player, "health", just_number(100));
just_set_var(j, "player", player);
// Execute script
just_eval_file(j, "game_logic.just");
// Get result
Value *result = just_get_var(j, "result");
// Call a specific function repeatedly without re-parsing (e.g. per frame)
Value *args[1] = { just_number(0.016) };
just_call(j, "update", args, 1);
just_destroy(j);C API Functions:
| Function | Description |
|---|---|
just_init() |
Create interpreter state with full capabilities |
just_init_ex(capabilities) |
Create interpreter state with a specific capability set |
just_destroy(j) |
Destroy interpreter |
just_eval(j, code) |
Execute string |
just_eval_file(j, filename) |
Execute file |
just_call(j, func_name, args, argc) |
Call an already-defined function directly, no re-parsing |
just_register_function(j, name, func) |
Register C function |
just_get_var(j, name) |
Get variable |
just_set_var(j, name, val) |
Set variable |
just_set_const(j, name, val) |
Set constant |
just_number(n) / just_string(s) / just_bool(b) / just_null() |
Create primitive values |
just_object_new(j) / just_object_set/get/has |
Create and manipulate objects |
just_array_new(j) / just_array_push/get/set/length |
Create and manipulate arrays |
just_to_json(j, v) / just_from_json(j, json) |
Convert between Value* and JSON strings |
just_to_string(j, v) |
Stringify a value |
just_gc_collect(j) |
Run garbage collector |
just_gc_set_threshold(j, n) |
Set allocations-before-auto-GC threshold |
just_gc_get_count(j) / just_gc_get_allocations(j) |
Inspect GC state |
just_set_max_iterations/call_depth/tokens(j, n) |
Set resource limits |
just_print_state(j) |
Dump interpreter state to stdout (debugging) |
just_version() / just_version_major/minor/patch() |
Query the linked library version |
All of the above are safe to call concurrently on the same JustState from multiple threads (see Thread Safety).
Write native functions in C and call them from Just. Plugin loading is thread-safe. Plugins are entirely separate from the core build — the core interpreter never needs to know a plugin exists until load_plugin() is called at runtime.
myplugin.c:
#include "just.h"
Value* double_number(JustState *j, Value **args, int count) {
double x = args[0]->data.number;
return just_number(x * 2);
}
void init_plugin(void (*register_func)(const char*, NativeFunc)) {
register_func("double", double_number);
}Build it as a shared library (gcc -shared -fPIC myplugin.c -o myplugin.so) and, importantly, build the core just executable itself with -rdynamic (already the default in build.bat and the commands below) — without it, any plugin that calls back into just_* functions (i.e. almost any useful plugin) fails to load with a silent "undefined symbol" error.
In Just:
load_plugin("myplugin")
print(double(21)) // 42Gated by JUST_CAP_PLUGIN — see Security & Sandboxing.
Just's core (just.c/just.h/main.c) always compiles with a single command — no CMake, no complicated build system. This assumes you keep the extra libraries (SQLite, mbedTLS, regex) in a src/ folder next to just.c, the same way this project is already set up. SQLite and HTTPS are optional — turn them on or off by adding/removing one flag. Regex is always included, no setup needed.
Important: All build commands assume you're in the src/ folder. First do:
cd srcSmallest binary (~167 KB). Good starting point if you're not sure yet what you need.
# Linux
gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_SQLITE -DJUST_NO_TLS just.c main.c -o just -ldl -lm
# Windows (MinGW)
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_SQLITE -DJUST_NO_TLS just.c main.c -o just.exe -lws2_32 -lm
Make sure src/sqlite3.c and src/sqlite3.h are present.
# Linux
gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_TLS just.c main.c -o just -ldl -lm
# Windows (MinGW)
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_TLS just.c main.c -o just.exe -lws2_32 -lm
-DJUST_NO_SQLITEis not passed — SQLite is included automatically whensqlite3.cis found.
# Linux
gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_SQLITE -Imbedtls/include just.c main.c \
-o just -Lmbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto -ldl -lm
# Windows (MinGW)
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_SQLITE -Imbedtls\include just.c main.c \
-o just.exe -Lmbedtls\library -lmbedtls -lmbedx509 -lmbedcrypto -lws2_32 -lm
-DJUST_NO_TLSis not passed — HTTPS is included when mbedTLS libraries are present.
Full build (~1.93 MB). Requires both src/sqlite3.c and built mbedTLS libraries.
# Linux
gcc -std=c11 -O2 -pthread -rdynamic -Imbedtls/include just.c main.c \
-o just -Lmbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto -ldl -lm
# Windows (MinGW)
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -Imbedtls\include just.c main.c \
-o just.exe -Lmbedtls\library -lmbedtls -lmbedx509 -lmbedcrypto -lws2_32 -lm
Neither
-DJUST_NO_SQLITEnor-DJUST_NO_TLSis passed — both are included.
| Variant | Binary size | Linux command | Windows command |
|---|---|---|---|
| Core only | ~167 KB | gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_SQLITE -DJUST_NO_TLS just.c main.c -o just -ldl -lm |
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_SQLITE -DJUST_NO_TLS just.c main.c -o just.exe -lws2_32 -lm |
| + SQLite | ~1.22 MB | gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_TLS just.c main.c -o just -ldl -lm |
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_TLS just.c main.c -o just.exe -lws2_32 -lm |
| + HTTPS | ~902 KB | gcc -std=c11 -O2 -pthread -rdynamic -DJUST_NO_SQLITE -Imbedtls/include just.c main.c -o just -Lmbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto -ldl -lm |
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -DJUST_NO_SQLITE -Imbedtls\include just.c main.c -o just.exe -Lmbedtls\library -lmbedtls -lmbedx509 -lmbedcrypto -lws2_32 -lm |
| Full (SQL + HTTPS) | ~1.93 MB | gcc -std=c11 -O2 -pthread -rdynamic -Imbedtls/include just.c main.c -o just -Lmbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto -ldl -lm |
gcc -std=c11 -O2 -pthread -Wl,--export-all-symbols -Imbedtls\include just.c main.c -o just.exe -Lmbedtls\library -lmbedtls -lmbedx509 -lmbedcrypto -lws2_32 -lm |
build.bat
It asks two simple yes/no questions (SQLite? HTTPS?) and builds just.exe for you — no need to remember any of the commands above. If something's missing, it tells you clearly what and where, instead of a confusing wall of linker errors.
Add -DJUST_NO_SQLITE and/or -DJUST_NO_TLS to any command above if you want to build without them even though the files are present.
Just is under active development, now at version 1.0.0 — the first public release. It handles real tasks — HTTP(S) requests, JSON processing, file I/O, SQLite databases, regex, C plugins — with structured error handling, closures, thread safety, and a capability-based sandbox for running untrusted scripts safely. The entire language can still be learned in a couple of evenings. Fully open to contributions: libraries, plugins, documentation, ideas.
- First public release — versioning reset to 1.0.0
- Regex, built into the core with zero setup
- HTTPS, optional at build time via mbedTLS (certificate verification not yet implemented — see HTTPS)
- Thread-safe interpreter instances — one
JustStatecan now be called safely from multiple threads just_call()— call a Just function from C repeatedly without re-parsing a call string each time- Structured error handling —
try/catch/finally/throw, catchable division-by-zero and stack-overflow errors instead of crashes - Closures & lambdas —
func(x) { ... }expressions with capture-by-value - Capability-based sandboxing —
just_init_ex()withJUST_CAP_EXEC/FILES/NET/PLUGIN/DB - Resource limits — configurable iteration, call-depth, and token caps for untrusted scripts
- Expanded standard library —
reduce,find,slice,concat,unique,sum,clamp,pad_start/pad_end,entries,merge, and more - Negative array indexing —
arr[-1] - Zero-dependency SQLite, still optional at build time
- Single binary, still no runtime, no package manager
- C embeddable, still easy to integrate into any C/C++ project
Bugs, suggestions, and contributions are welcome.
MIT © 2026 T-bit/BDD — Beyond Digital Dominance