Skip to content

cly-lang/cly.h

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

22 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

image

cly.hβ„’ πŸŒ™

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.



Implementation

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-slot value stack for expression evaluation
  • 256-slot locals array for function variables
  • 64-level call stack (IP, BP, SP saved per frame)
  • 32-level exception 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 System

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.

Language Syntax

Variables

const x = 10;        // immutable
mut y = 20;          // mutable
y = 30;              // reassignment
y += 5;              // compound assignment: += -= *= /= %= &= |= ^= <<= >>=

Block-scoped with shadowing. Uninitialized variables default to 0.

Types and Literals

42                   // int
3.14                 // float
true, false          // bool
"hello"              // string
[1, 2, 3]            // list
{x: 10, y: 20}       // map

Operators

Arithmetic: + - * / % ** (power)
Bitwise: & | ^ ~ << >>
Comparison: == != < > <= >=
Logical: ! && || Unary: - (negation) ! (logical not) ~ (bitwise not)

Operator precedence follows C conventions.

Control Flow

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);
}

Functions

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.

Structs

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 assignment

Structs are nominal types. Maximum 16 fields per struct. Maximum 32 struct types per program.

Lists

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));                         // length

Dynamic arrays with automatic growth. Negative indices count from end (-1 is last element).

Maps

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 key

Hash tables with open addressing. Keys can be int, float, bool, or string. Resize at 75% load factor.

Strings

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";     // concatenation

Immutable, refcounted. No UTF-8 validation - byte arrays. Escape sequences: \n \t \r \\ \" \{ \} in f-strings.

Exception Handling

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.

Comments

// line comment

/* block comment */

/* multi-line
   block comment */

Built-in Functions

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

C API

Setup

#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);

Status Codes

CLY_OK         // success
CLY_ERRRUN     // runtime error
CLY_ERRMEM     // allocation failure
CLY_ERRTYPE    // type error
CLY_ERRSYNTAX  // compilation error

State Management

ClyState *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 Operations

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);

Pushing Values

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);

Type Checking

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);

Getting Values

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);

Checked Access (raises error on type mismatch)

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);

Registering C Functions

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);

Global Variables

ClyType cly_getglobal(ClyState *I, const char *name);
void cly_setglobal(ClyState *I, const char *name);

Table Operations

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);

Limits

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.

Memory Characteristics

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.

Testing

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_cly

Performance

Considering 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.

Platform Support

Requires C99 standard library. Tested on:

  • Linux (x86_64, ARM64)
  • Windows (MSVC, MinGW, WSL)
  • macOS (x86_64, ARM64)

Thread Safety

Not thread-safe. Use one ClyState per thread or provide external synchronization.

Known Limitations

  • 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.

License

GPL-3.0-only

Copyright (C) 2026 Ethan Alexander

About

πŸŒ™ cly.h is a single-file, embeddable, interpreted language built for dynamic configuration. Programmable like code, flexible like JSON, and incredibly tiny in memory footprint.

Topics

Resources

License

Stars

Watchers

Forks

Contributors