cly.h is a single-file, embeddable, interpreted language built for dynamic configuration - programmable like code, flexible like JSON.
Cly's not here to get in your way, nor is he here to offer a 1000 builtin standard functions, he's here to help encapsulate a higher level connection between the user and the program.
Cly has one purpose; nothing more, nothing less. He's here to offer extensibility at ease, without hindering flexibility.
Compiler Design: Single-pass, like C. Unfortunately meaning we can only procedurally reference data. Uses Recursive descent parser emitting bytecode directly. No AST construction. First error terminates compilation. Line numbers tracked for runtime stack traces.
Virtual Machine:
256-slotvalue stack for expression evaluation256-slotlocals array for function variables64-levelcall stack (IP, BP, SP saved per frame)32-levelexception handler stack- Reference counting for heap-allocated types
Memory Management: ref counting on strings, lists, maps, and structs. ref count gets incremented on copy, and decremented on overwrite or scope exit. On zero refcount, we trigger immediate deallocation. A mark-and-sweep cycle collector tracks all lists, maps, and structs via intrusive linked lists, running automatically when allocation count exceeds a dynamic threshold and at shutdown to collect circular references.
Bytecode: around 50 instructions, pretty simple. Stack-based execution with operands encoded inline. No constant pool for most values.
| Type | Size | Notes |
|---|---|---|
| int | 64-bit signed | Full range: -2^63 to 2^63-1 |
| float | 64-bit IEEE 754 | double precision |
| bool | 1 byte | true/false |
| string | heap | immutable, refcounted, no interning (might be a future idea, don't know enough about it) |
| list | heap | dynamic array, refcounted |
| map | heap | hash table, refcounted |
| struct | heap | nominal type, refcounted |
| function | index | reference to function in program |
Value representation: A tagged union with 8-byte type field and 8-byte data field (16 bytes per value on stack). This seems to be a pretty standard approach as C doesn't have many other solutions.
const x = 10; // immutable
mut y = 20; // mutable
y = 30; // reassignment
y += 5; // compound assignment: += -= *= /= %= &= |= ^= <<= >>=Block-scoped with shadowing. Uninitialized variables default to 0.
42 // int
3.14 // float
true, false // bool
"hello" // string
[1, 2, 3] // list
{x: 10, y: 20} // mapArithmetic: + - * / % ** (power)
Bitwise: & | ^ ~ << >>
Comparison: == != < > <= >=
Logical: ! && ||
Unary: - (negation) ! (logical not) ~ (bitwise not)
Operator precedence follows C conventions.
if (condition) {
// ...
} else if (other) {
// ...
} else {
// ...
}
while (condition) {
if (done) { break; }
if (skip) { continue; }
}
for (const i in 0..10) { // range: [0, 10)
println(i);
}
for (const item in collection) { // iterate list or map keys
println(item);
}fn add(a, b) {
return a + b;
}
const sub = fn(a, b) { return a - b; }; // anonymous function
fn recurse(n) {
if (n <= 0) { return 1; }
return n * recurse(n - 1);
}Functions are first-class values. Maximum 8 parameters per function (configurable at compile time). No closures over outer scope variables.
struct Point { x, y }
const p1 = Point(3, 4); // positional construction
const p2 = Point{ y: 10, x: 5 }; // named construction
println(p1.x); // field access
mut p3 = Point(0, 0);
p3.x = 7; // field assignmentStructs are nominal types. Maximum 16 fields per struct. Maximum 32 struct types per program.
mut list = [1, 2, 3];
println(list[0]); // indexing: 1
println(list[-1]); // negative indexing: 3
list[0] = 10; // assignment
push(list, 4); // append
const val = pop(list); // remove and return last
println(len(list)); // lengthDynamic arrays with automatic growth. Negative indices count from end (-1 is last element).
const m = {
name: "cly",
"port": 8080,
42: "answer"
};
println(m.name); // dot access for identifier keys
println(m["port"]); // bracket access for any key
println(m[42]); // numeric key
mut config = {};
config.debug = true; // add new key
config["host"] = "localhost";
println(has(m, "name")); // check key existence
println(keys(m)); // get all keys as list
println(values(m)); // get all values as list
remove(m, "name"); // delete keyHash tables with open addressing. Keys can be int, float, bool, or string. Resize at 75% load factor.
const s = "hello";
println(len(s)); // length in bytes
const name = "world";
println(f"Hello, {name}!"); // f-string interpolation
println(f"2 + 2 = {2 + 2}"); // expressions in braces
const concat = "hello" + " " + "world"; // concatenationImmutable, refcounted. No UTF-8 validation - byte arrays. Escape sequences: \n \t \r \\ \" \{ \} in f-strings.
try {
if (error_condition) {
throw "error message";
}
println("success");
} catch (e) {
println(f"caught: {e}");
}Exception values can be any type. Unhandled exceptions print stack trace and terminate execution. Stack unwinding decrefs values properly to prevent leaks.
// line comment
/* block comment */
/* multi-line
block comment */| Function | Arguments | Returns | Description |
|---|---|---|---|
print(val) |
any | nil | Print without newline |
println(val) |
any | nil | Print with newline |
len(val) |
string/list/map | int | Length/count |
push(list, val) |
list, any | nil | Append to list |
pop(list) |
list | any | Remove and return last element (0 if empty) |
peek(list) |
list | any | Return last element without removing (0 if empty) |
has(map, key) |
map, any | bool | Check if key exists |
keys(map) |
map | list | Get all keys |
values(map) |
map | list | Get all values |
remove(map, key) |
map, any | nil | Delete key from map |
int(val) |
any | int | Convert to integer |
float(val) |
any | float | Convert to float |
#define CLY_IMPLEMENTATION
#include "cly.h"
ClyState *I = cly_open();
ClyStatus s = cly_loadstring(I, "println(42);");
if (s == CLY_OK) {
s = cly_run(I);
}
cly_close(I);CLY_OK // success
CLY_ERRRUN // runtime error
CLY_ERRMEM // allocation failure
CLY_ERRTYPE // type error
CLY_ERRSYNTAX // compilation errorClyState *cly_open(void);
void cly_close(ClyState *I);
ClyStatus cly_loadstring(ClyState *I, const char *source);
ClyStatus cly_loadfile(ClyState *I, const char *path);
ClyStatus cly_run(ClyState *I);
const char *cly_error(ClyState *I);Stack indices are 1-based. Negative indices count from top (-1 is top of stack).
int cly_gettop(ClyState *I);
void cly_settop(ClyState *I, int index);
void cly_pop(ClyState *I, int n);
void cly_pushvalue(ClyState *I, int index);void cly_pushnil(ClyState *I);
void cly_pushinteger(ClyState *I, int64_t n);
void cly_pushfloat(ClyState *I, double n);
void cly_pushboolean(ClyState *I, bool b);
void cly_pushstring(ClyState *I, const char *s);
void cly_pushlstring(ClyState *I, const char *s, size_t len);bool cly_isinteger(ClyState *I, int index);
bool cly_isfloat(ClyState *I, int index);
bool cly_isstring(ClyState *I, int index);
bool cly_isboolean(ClyState *I, int index);
ClyType cly_type(ClyState *I, int index);int64_t cly_tointeger(ClyState *I, int index);
double cly_tofloat(ClyState *I, int index);
bool cly_toboolean(ClyState *I, int index);
const char *cly_tostring(ClyState *I, int index);
size_t cly_rawlen(ClyState *I, int index);int64_t cly_checkinteger(ClyState *I, int arg);
double cly_checkfloat(ClyState *I, int arg);
const char *cly_checkstring(ClyState *I, int arg);
bool cly_checkboolean(ClyState *I, int arg);typedef int (*ClyCFunction)(ClyState *I);
void cly_register(ClyState *I, const char *name, ClyCFunction fn, int nparams);C function example:
static int cfunc_add(ClyState *I) {
int64_t a = cly_checkinteger(I, 1);
int64_t b = cly_checkinteger(I, 2);
cly_pushinteger(I, a + b);
return 1; // number of return values
}
cly_register(I, "add", cfunc_add, 2);ClyType cly_getglobal(ClyState *I, const char *name);
void cly_setglobal(ClyState *I, const char *name);void cly_newtable(ClyState *I);
void cly_getfield(ClyState *I, int index, const char *field);
void cly_setfield(ClyState *I, int index, const char *field);
ClyType cly_tableget(ClyState *I, int index);
void cly_tableset(ClyState *I, int index);
int cly_tablelen(ClyState *I, int index);| Resource | Limit | Location |
|---|---|---|
| Call stack depth | 64 | VM.frames |
| Locals per function | 64 | ClyFunction.locals |
| Parameters per function | 8 | ClyFunction.params |
| String constants | 256 | ClyProgram.strings |
| Functions per program | 64 | ClyProgram.funcs |
| Struct types | 32 | ClyProgram.struct_types |
| Struct fields | 16 | StructType.fields |
| Global variables | 128 | ClyState.globals |
| Exception handlers | 32 | VM.handlers |
| Value stack size | 256 | VM.stack |
All limits are compile-time constants. Modify source to adjust.
Baseline overhead: ~100KB for VM state and empty program.
Value size: 16 bytes per value on stack (8-byte type tag + 8-byte data/pointer).
String overhead: 16 bytes per string object + string data + 1 null terminator.
List overhead: 16 bytes per list object + capacity * 16 bytes for elements.
Map overhead: 24 bytes per entry (key value + occupancy flags) * capacity. Resizes at 75% load.
Struct overhead: 16 bytes per struct object + field_count * 16 bytes for fields.
Many test cases covering all language features, edge cases, and memory safety. Run with AddressSanitizer:
gcc -g -fsanitize=address -o test_cly tests/main.c tests/test_*.c && ./test_clyConsidering this is a bytecode interpreter, expect 10-50x slower than native C depending on operation type. Suitable for scripting, configuration, and high-level logic. Probably not suitable for performance-critical inner loops. As of current, I have no concrete data about speeds compared to compiled languages as they have 10's of years of compiler optimizations that would shield us from seeing its raw speed.
Compilation is O(n) in source size. No optimization passes.
Requires C99 standard library. Tested on:
- Linux (x86_64, ARM64)
- Windows (MSVC, MinGW, WSL)
- macOS (x86_64, ARM64)
Not thread-safe. Use one ClyState per thread or provide external synchronization.
- No string interning. Identical string literals create separate allocations.
- No tail call optimization. Deep recursion overflows call stack.
- No module system. All code executes in global scope.
- Strings are byte arrays. No UTF-8 validation or unicode support.
- No regex support.
- No standard library beyond built-in functions.
GPL-3.0-only
Copyright (C) 2026 Ethan Alexander