Repository navigation
Reference
See the Wiki Home for detailed documentation, guides, and language reference.
- Core runtime: lexer, parser, AST, VM, and a small standard library.
-
Sample CLI (
src/main.c): demonstrates embedding and registeringfm.*natives. -
Public headers:
src/cupidscript.his the embedder-facing API. - Examples/tests: small scripts that exercise language features and stdlib.
Modern Syntax
-
Walrus Operator:
if ((result := compute()) > 10) { use(result); }- assign and use in same expression - List/Map Comprehensions:
[x*2 for x in nums if x > 5],{k: v for k, v in map} - Pattern Matching:
match value { 0 => "zero", n if n > 10 => "big", _ => "other" } - Pipe Operator:
data |> filter(fn) |> map(transform) |> reduce(sum) - Optional Chaining:
user?.profile?.name ?? "Guest" - String Interpolation:
"Hello ${name}, you are ${age} years old" - Destructuring:
let [a, b, ...rest] = list; let {x, y} = point; - Spread/Rest:
[0, ...xs, 10],fn sum(...nums) { } - Arrow Functions:
fn(x) => x * 2 - Raw Strings:
r"C:\path\to\file"(no escape processing)
Object-Oriented Programming
- Classes with single inheritance and
supercalls - Structs with positional/named field construction
- Enums as named integer constants
- Tuples (immutable):
(10, 20)or(x: 10, y: 20)
Functional Programming
- First-class functions and closures
- Generators with
yield - Higher-order functions:
filter,map_list,reduce,zip - Tail recursion optimization (for simple recursive patterns)
Async & Error Handling
-
async/awaitsyntax (synchronous execution currently) - Structured error objects with stack traces
-
try/catch/finallyblocks - Optional chaining for nil-safe access
Standard Library
- Data: JSON, YAML 1.2.2, CSV, XML parsing/generation
- Collections: List, map, and string helpers
- Files: Filesystem operations, glob patterns, file watching
- Network: HTTP client (with TLS via OpenSSL)
- Regex: Full regex matching and replacement
- Process: Subprocess execution and management
- Archives: TAR and ZIP creation/extraction
- Safety: Instruction limits, timeouts, memory controls
make
bin/cupidscript examples/features.csTLS/HTTPS support requires OpenSSL development libraries.
-
Linux (Debian/Ubuntu):
libssl-dev -
Linux (Fedora/RHEL):
openssl-devel
To build without TLS, set CS_NO_TLS=1:
make CS_NO_TLS=1Tests are plain .cs scripts under tests/ that use the stdlib assert(...) function.
make test-
Create a new test file in
tests/with a.csextension (e.g.tests/my_feature.cs). -
Write assertions with
assert(condition, "message")so failures are clear. - Print a success marker at the end (optional, but helps when reading logs).
Example:
// tests/my_feature.cs
let x = 1 + 2;
assert(x == 3, "basic math");
print("my_feature ok");If a test should fail (e.g. parse/runtime errors), add this header at the top:
// EXPECT_FAILExample:
// tests/negative_example.cs
// EXPECT_FAIL
assert(false, "should fail");Files prefixed with _ are ignored by the test runner. Use these for shared helpers or fixtures.
Other useful scripts:
bin/cupidscript examples/test.csbin/cupidscript examples/stress.csbin/cupidscript examples/stacktrace.csbin/cupidscript examples/trycatch.csbin/cupidscript examples/try_finally.csbin/cupidscript examples/throwtrace.csbin/cupidscript examples/closures.csbin/cupidscript examples/time.csbin/cupidscript examples/benchmark.csbin/cupidscript examples/gc_cycle.csbin/cupidscript examples/gc_auto.csbin/cupidscript examples/safety_config.csbin/cupidscript examples/safety_test.csbin/cupidscript examples/filesystem.csbin/cupidscript examples/json.csbin/cupidscript examples/json_example.csbin/cupidscript examples/list_helpers.csbin/cupidscript examples/arrow_functions.csbin/cupidscript examples/spread_rest.csbin/cupidscript examples/pipe_operator.csbin/cupidscript examples/classes.csbin/cupidscript examples/default_params.csbin/cupidscript examples/match_expr.csbin/cupidscript examples/match_patterns.csbin/cupidscript examples/destructuring.csbin/cupidscript examples/structs.csbin/cupidscript examples/enums.csbin/cupidscript examples/generators.cs
CupidScript is intentionally small: a tree-walk interpreter with dynamic values and a C embedding API. Its language features are described below.
-
Variables & Functions:
letdeclarations, function literals, arrow functions=>, closures, default parameters -
Classes & Inheritance: First-class classes with
selfbinding, single inheritance withsuper - Structs & Enums: Fixed-field structs with positional construction, enums as named integer constants
-
Pattern Matching:
matchexpressions with destructuring, guards, and type patterns -
Comprehensions: List and map comprehensions with filtering:
[x*2 for x in nums if x > 5] - Tuples: Immutable ordered collections with named/positional fields
-
Async/Await:
asyncfunctions andawaitexpressions (currently synchronous) -
Generators: Functions using
yieldto produce sequences -
Error Handling:
try/catch/finally, structured error objects -
Module System:
require()for modules withexportmaps,load()for script execution -
Modern Operators: Pipe
|>, spread/rest..., optional chaining?., nullish coalescing??, ranges..and..=
let name = expr; // declaration (optional initializer)
name = expr; // assignment (must already exist)
name += expr; // compound assignment (also -=, *=, /=)
fn add(a, b) { // function definition
return a + b;
}
fn add2(a, b) => a + b; // arrow function
fn greet(name, greeting = "Hello") {
return greeting + ", " + name + "!";
}
async fn add(a, b) { return a + b; }
let sum = await add(2, 3);
if (cond) { ... } else { ... }
while (cond) { ... }
for x in expr { ... } // for-in over lists/maps
for (init; cond; incr) { ... } // C-style for loop
break;
continue;
return expr;
throw expr;
try { ... } catch (e) { ... } finally { ... }
export name = expr; // module exports
switch (expr) { case 1 { ... } default { ... } }
// structs + enums
struct Point { x, y = 0 }
enum Color { Red, Green = 5, Blue }
// tuples
let point = (10, 20); // positional tuple
let named = (x: 10, y: 20); // named tuple
print(point[0], named.x); // 10 10
// generators
fn range(n) {
let i = 0;
while (i < n) { yield i; i += 1; }
}
// classes
class File {
fn new(path) { self.path = path; }
fn is_hidden() { return starts_with(self.path, "."); }
}
class ImageFile : File {
fn new(path) { super.new(path); self.is_img = ends_with(path, ".png"); }
fn is_image() { return self.is_img; }
}
// pattern matching
let result = match value {
0 => "zero",
1 | 2 => "one or two",
n if n > 10 => "big",
_ => "other"
};
// comprehensions
let evens = [x for x in range(10) if x % 2 == 0];
let squares = {x: x*x for x in nums};
let items = {k: v for k, v in map if v != nil};Assignment rule (intentional): let creates new variables; plain assignment (x = ...) errors if x was never declared. This prevents silent typos like coutn = 1.
- Operators with precedence:
||,&&,==,!=,<,<=,>,>=,+,-,*,/,%, unary!, unary- -
Walrus operator:
:=assigns and returns a value in one expression:if ((x := compute()) > 10) { use(x); } - Nullish coalescing:
??returns right operand if left isnil - Pipe operator:
|>passes left value into right call, supports_placeholder - Range operators:
start..end(exclusive),start..=end(inclusive) - Ternary:
cond ? a : b - Optional chaining:
obj?.fieldreturnsnilifobjisnil - Strings:
"..."with escapes\0,\xHH,\n,\t,\r,\",\\ - Raw strings:
r"..."with no escape processing - String interpolation:
"value is ${x + 1}" - Integers: decimal (
123), hex (0xFF), underscores (1_000_000) - Floats: decimal with dot (
3.14), scientific notation (1.5e-3,2e10) - Literals: list
[1, 2, "hi"], map{ "a": 1, "b": 2 }, tuple(1, 2)or(x: 1, y: 2) - Spread in literals:
[0, ...xs],{...m1, ...m2} - Comprehensions:
[expr for var in iter if cond],{k: v for var in iter if cond} - Dynamic values:
nil,true/false, integers, floats, strings, functions, native functions, lists, maps, strbuf, tuples - Structs: fixed-field maps with positional construction
- Enums: named integer constants stored in a map
- Generators: functions that use
yieldand return a list of yielded values - Async/Await:
asyncfunctions andawait(currently synchronous execution)
Notes:
-
+supportsint + int,float + float, mixed arithmetic (int+float→float), and string concatenation (if either operand is a string). -
/division always returns float for precision -
&&/||short-circuit.
Lists and maps are first-class dynamic values. Comprehensions provide concise syntax for creating collections.
let xs = [10, 20];
let ys = [0, ...xs, 30];
xs[1] = 99;
print(xs[0], xs[1], len(xs)); // 10 99 2
let m = { "name": "cupid" };
let m2 = {"name": "cupid", "version": 1};
let m3 = {...m, ...m2};
print(m["name"]); // cupid
print(keys(m)); // list of keys
// List comprehensions
let evens = [x for x in range(20) if x % 2 == 0];
let doubled = [x * 2 for x in [1, 2, 3]];
// Map comprehensions
let squares = {x: x*x for x in range(5)};
let filtered = {k: v for k, v in map if v != nil};
// Iterate over strings
let chars = [c for c in "hello"]; // ["h", "e", "l", "l", "o"]let xs = [10, 20]; let ys = [0, ...xs, 30]; xs[1] = 99; print(xs[0], xs[1], len(xs)); // 10 99 2
let m = { "name": "cupid" }; let m2 = {"name": "cupid", "version": 1}; let m3 = {...m, ...m2}; print(m["name"]); // cupid print(keys(m)); // list of keys
List helpers:
```cs
extend(xs, ys); // append elements
index_of(xs, 42); // index or -1
sort(xs); // insertion (default)
sort(xs, "quick"); // quicksort
sort(xs, "merge"); // mergesort
filter(xs, fn(x) => x > 5); // keep matching elements
map_list(xs, fn(x) => x*2); // transform elements
reduce(xs, fn(acc, x) => acc + x, 0); // fold
Indexing rules:
-
list[int]-> element ornilif out of range -
map[string]-> value ornilif missing -
tuple[int]-> element by position -
tuple.field-> element by name (named tuples) - Assignment supports
xs[i] = vandm["k"] = v
Practical plugin pattern (structured state/config):
let state = map();
state["enabled"] = true;
state["bindings"] = list();
push(state["bindings"], "F5");Map keys can be any value; equality follows the same rules as == (so 1 and 1.0 collide).
Stdlib provides:
-
load("file.cs"): executes another script file every time it’s called -
require("file.cs"): executes a file once per VM and returns itsexportsmap -
require_optional("file.cs"): likerequire, but returnsnilif file doesn't exist (no error) -
cwd()/chdir(path)to inspect or change the VM's current directory
Paths are resolved relative to the currently-running script file’s directory.
Module pattern:
// lib.cs
export hello = fn(name) { return "hello " + name; };
// main.cs
let lib = require("lib.cs");
print(lib.hello("world"));obj.field is supported as field access:
- If
objis a map,obj.fieldreturns the same value asobj["field"].
obj.method(a, b) is supported as a method call:
- If
objis a map,obj.method(...)calls the function stored atobj["method"](no implicitselfargument). -
strbufalso exposes methods likeb.append(...),b.str(),b.len(),b.clear().
Compatibility note: CupidFM-style dotted globals still work (e.g. fm.status("hi")) even if fm is not a script value; the VM falls back to looking up a global named "fm.status".
Classes are first-class values and are called to create instances. Methods are invoked on instances, with self bound automatically.
class File {
fn new(path) { self.path = path; }
fn is_hidden() { return starts_with(self.path, "."); }
}
class ImageFile : File {
fn new(path) { super.new(path); self.is_img = ends_with(path, ".png"); }
fn is_image() { return self.is_img; }
}
let img = ImageFile("cat.png");
print(img.is_image());Functions can collect extra arguments into a list:
fn log_all(prefix, ...items) {
for item in items { print(prefix, item); }
}fn add(a, b) { return a + b; }
print(10 |> add(5)); // add(10, 5)
print(10 |> add(5, _)); // add(5, 10)Collections:
-
list,map,len,push,pop,extend,index_of,insert,remove,slice,keys,values,items,map_values -
reverse,reversed,contains,copy,deepcopy,sort(list, [cmp], [algo]) -
filter,map_list,reduce,zip,enumerate,flatten - Comprehensions:
[expr for var in iter if cond],{k: v for var in iter if cond}
Strings:
-
str_find,str_replace,str_split,substr,join,to_str,to_int,to_float -
trim/ltrim/rtrim,lower/upper,starts_with/ends_with,str_repeat,split_lines - String interpolation:
"result: ${x + 1}" - Raw strings:
r"\path\to\file"(no escape processing)
Data Formats:
-
JSON:
json_parse(text),json_stringify(value) -
YAML:
yaml_parse(text),yaml_stringify(value)(YAML 1.2.2 compliant) -
CSV:
csv_parse(text),csv_stringify(rows) -
XML:
xml_parse(text)returns document tree
Filesystem & Paths:
-
read_file,write_file,exists,is_dir,is_file,list_dir,mkdir,rm,rename -
path_join,path_dirname,path_basename,path_ext,cwd,chdir -
glob(pattern)for file pattern matching -
file_watch(path, callback)for filesystem monitoring
Network & I/O:
-
http_get,http_post,http_requestwith validated custom headers and string bodies -
subprocess_run,subprocess_spawnfor external process execution - Archive support:
create_tar,extract_tar,create_zip,extract_zip
Regex:
-
regex_match(pattern, text),regex_find_all(pattern, text),regex_replace(pattern, text, replacement)
Time & Safety:
-
now_ms,sleep,date_timefor parsing/formatting timestamps -
set_timeout,set_instruction_limit,get_timeout,get_instruction_limit,get_instruction_count
Errors:
-
error,is_error,format_error,ERRmap
Functions are values, so you can pass them to native APIs as callbacks:
fn on_key(key) {
print("key:", key);
}
// Example: fm.on("key", on_key)CupidScript also supports closures (functions capturing variables) and anonymous function literals:
fn make_counter() {
let n = 0;
return fn() { n = n + 1; return n; };
}
let c = make_counter();
print(c(), c(), c()); // 1 2 3-
src/cs_value.c,src/cs_lexer.c,src/cs_parser.c,src/cs_vm.c,src/cs_stdlib.c– core runtime and standard library implementations. -
src/main.c– sample program showing how to bootstrap the VM and expose native functions (thefm.*API in this project). -
Headers:
src/cupidscript.h,src/cs_vm.h,src/cs_value.h– public API and value types. -
Makefile– simple build system producing a library and a small executable.
- A C99-compliant compiler (gcc/clang) and a POSIX-like toolchain.
-
make(as specified by the provided Makefile).
make allThis should produce:
-
libcupidscript.ainbin/ -
cupidscript(executable) inbin/
Use the Makefile to keep the networking sources and their dependencies together. Custom compiler and linker flags can be passed through CFLAGS and LDFLAGS; TLS flags and libraries remain included. Set CS_NO_TLS=1 to omit OpenSSL.
- Create a VM:
cs_vm* vm = cs_vm_new();
- Register stdlib:
cs_register_stdlib(vm);
- Expose natives:
cs_register_native(vm, "my.native", my_fn, NULL);
- Run code from file:
Or run from a string:
cs_vm_run_file(vm, "script.cs");
cs_vm_run_string(vm, code, "<string>");
- Call a named script function:
Or call a function value (callback) you previously stored:
cs_call(vm, "function_name", argc, argv, &out);
cs_call_value(vm, fn_value, argc, argv, &out);
- Retrieve errors:
const char* err = cs_vm_last_error(vm);
- Strings:
cs_str(vm, "hello"); /* and convert to C with cs_to_cstr(out_val); */
-
Safety controls (prevent runaway scripts):
cs_vm_set_instruction_limit(vm, 10000000); // max 10M instructions cs_vm_set_timeout(vm, 5000); // max 5000ms execution cs_vm_interrupt(vm); // interrupt from another thread
-
GC auto-collect (optional automatic garbage collection):
cs_vm_set_gc_threshold(vm, 1000); // collect when tracked >= 1000 cs_vm_set_gc_alloc_trigger(vm, 100); // collect every 100 allocations
CupidScript includes built-in protection against runaway scripts that could hang the host application (CupidFM):
Limits the total number of VM operations (expressions evaluated) per script execution:
cs_vm_set_instruction_limit(vm, 10000000); // 10 million instructions- Default:
0(unlimited) - When exceeded, script aborts with error:
"instruction limit exceeded (N instructions)" - Counter resets at the start of each
cs_vm_run_file/cs_vm_run_stringcall - Get current count:
uint64_t count = cs_vm_get_instruction_count(vm);
Limits the wall-clock execution time per script run:
cs_vm_set_timeout(vm, 5000); // 5 second timeout- Default:
0(unlimited) - Time in milliseconds
- When exceeded, script aborts with error:
"execution timeout exceeded (N ms)" - Timer resets at the start of each script execution
- Checked every 1000 instructions to minimize overhead
Allows the host to request immediate termination of a running script (useful for UI cancel buttons):
cs_vm_interrupt(vm); // Thread-safe signal- Can be called from any thread while script is running
- Script aborts with error:
"execution interrupted by host" - Flag is automatically reset at the start of each script execution
cs_vm* vm = cs_vm_new();
cs_register_stdlib(vm);
// Protect against infinite loops in user plugins
cs_vm_set_instruction_limit(vm, 50000000); // 50M instructions
cs_vm_set_timeout(vm, 10000); // 10 second timeout
int rc = cs_vm_run_file(vm, "user_plugin.cs");
if (rc != 0) {
fprintf(stderr, "Plugin error: %s\n", cs_vm_last_error(vm));
fprintf(stderr, "Instructions executed: %llu\n",
(unsigned long long)cs_vm_get_instruction_count(vm));
}See examples/safety_demo.c for a complete demonstration.
Scripts can also query and configure their own safety limits:
// Get current limits
let timeout = get_timeout(); // milliseconds
let limit = get_instruction_limit(); // instruction count
let count = get_instruction_count(); // current count
// Set stricter limits for a critical section
set_timeout(1000); // 1 second
set_instruction_limit(10000000); // 10M instructions
// Heavy processing...See examples/safety_config.cs and examples/safety_test.cs for demonstrations.
The C API provides retained values for lists, maps, and tuples. See the embedding guide for ownership rules and a complete host example.
cs_value mylist = cs_list(vm);
cs_list_push(mylist, cs_int(42));
cs_value greeting = cs_str(vm, "hello");
cs_list_push(mylist, greeting);
cs_value_release(greeting);
size_t len = cs_list_len(mylist);
cs_value val = cs_list_get(mylist, 0);
cs_list_set(mylist, 1, cs_float(3.14));
cs_value last = cs_list_pop(mylist);
cs_value_release(val);
cs_value_release(last);
cs_value_release(mylist);cs_value mymap = cs_map(vm);
cs_value label = cs_str(vm, "Alice");
cs_map_set(mymap, "name", label);
cs_value_release(label);
cs_map_set(mymap, "age", cs_int(30));
if (cs_map_has(mymap, "name")) {
cs_value name = cs_map_get(mymap, "name");
printf("Name: %s\n", cs_to_cstr(name));
cs_value_release(name);
}
cs_value keys = cs_map_keys(vm, mymap);
cs_map_del(mymap, "age");
cs_value_release(keys);
cs_value_release(mymap);All returned cs_value objects must be released with cs_value_release().
-
Value types (
cs_type, exposed viacupidscript.h):-
CS_T_NIL,CS_T_BOOL,CS_T_INT,CS_T_FLOAT,CS_T_STR,CS_T_LIST,CS_T_MAP,CS_T_STRBUF,CS_T_FUNC,CS_T_NATIVE
-
-
Core value wrapper:
cs_value { type; union { int b; int64_t i; double f; void* p; } as; }
-
Public helpers:
-
cs_vm_new,cs_vm_free -
cs_vm_run_file,cs_vm_run_string -
cs_vm_collect_cycles(collect list/map cycles) -
cs_vm_set_gc_threshold,cs_vm_set_gc_alloc_trigger(GC auto-collect) -
cs_vm_set_instruction_limit,cs_vm_set_timeout,cs_vm_interrupt,cs_vm_get_instruction_count(safety controls) -
cs_register_native,cs_call -
cs_call_value(call a function value from C) -
cs_vm_last_error/cs_error(get/set VM error from native code) -
cs_last_error(compat getter; returnsNULLif no error) -
cs_to_cstr,cs_nil,cs_bool,cs_int,cs_str,cs_str_take -
cs_list,cs_map,cs_strbuf -
cs_value_copy,cs_value_release(retain/release values for host storage)
-
CupidScript’s stdlib is implemented in src/cs_stdlib.c and registered via cs_register_stdlib(vm).
-
print(...)→ prints values to stdout -
assert(cond, "message")→ sets a VM error and aborts execution ifcondis falsy -
typeof(x)→ returns"nil" | "bool" | "int" | "float" | "string" | "list" | "map" | ... -
getenv("NAME")→ returns a string ornil
-
is_nil(x),is_bool(x),is_int(x),is_float(x)→ bool -
is_string(x),is_list(x),is_map(x),is_function(x)→ bool
-
load(path)→ executes another script file (every call) -
require(path)→ executes another script once per VM and returns itsexportsmap -
require_optional(path)→ likerequire, but returnsnilif file doesn't exist (no error)
-
range(end)→[0, 1, ..., end-1] -
range(start, end)→[start, ..., end-1] -
range(start, end, step)→[start, start+step, ..., < end](step can be negative)
-
list()→ new list -
len(list|string|map|strbuf)→ length -
push(list, value)→ append -
pop(list)→ pop last element (ornil) -
insert(list, idx, value)→ insert (clamped) -
remove(list, idx)→ remove and return element (ornil) -
slice(list, start, end)→ sub-list
-
map()→ new map -
mget(map, key)→ get value (ornil) -
mset(map, key, value)→ set value -
mhas(map, key)→ bool -
mdel(map, key)→ delete (bool) -
keys(map)→ list of keys (strings) -
values(map)→ list of values -
items(map)→ list of[key, value]pairs -
copy(list|map)→ shallow copy -
deepcopy(list|map)→ deep copy (recursive) -
reverse(list)→ reverse in-place -
reversed(list)→ return reversed copy -
contains(list|map|string, value|key)→ bool
Note: You can usually use indexing instead of mget/mset:
m["k"], m["k"] = v.
-
str_find(s, sub)→ index (or-1) -
str_replace(s, old, repl)→ new string -
str_split(s, sep)→ list of strings -
substr(s, start, len)→ substring -
join(list, sep)→ join list elements into a string -
str_trim(s)→ remove leading/trailing whitespace -
str_ltrim(s)→ remove leading whitespace -
str_rtrim(s)→ remove trailing whitespace -
str_lower(s)→ convert to lowercase -
str_upper(s)→ convert to uppercase -
str_startswith(s, prefix)→ bool -
str_endswith(s, suffix)→ bool -
str_repeat(s, count)→ repeat string N times
-
path_join(a, b)→ joined path (simple join) -
path_dirname(path)→ directory portion -
path_basename(path)→ basename -
path_ext(path)→ extension without dot (or"") Math functions -
abs(x)→ absolute value (preserves type: int→int, float→float) -
min(...values),max(...values)→ minimum/maximum of numbers -
floor(x),ceil(x),round(x)→ rounding to int -
sqrt(x)→ square root (returns float) -
pow(base, exp)→ exponentiation (returns float)
-
to_int(x)→ int (ornilif not convertible; floats truncate toward zero -
fmt("x=%d s=%s b=%b v=%v", ...)→ formatted string-
%dint,%sstring,%bbool,%vany value (best-effort),%%literal percent
-
-
to_int(x)→ int (ornilif not convertible) -
to_str(x)→ string
-
now_ms()→ current wall-clock time in milliseconds (int) -
sleep(ms)→ sleep formsmilliseconds (blocking; implemented via POSIXnanosleep)
-
error(msg)→ create error object with message and stack trace -
is_error(value)→ check if value is an error object
-
gc()→ manually collect list/map reference cycles (returns number of objects collected) -
gc_stats()→ returns map with GC statistics:{tracked, collections, collected, allocations} -
gc_config()→ get current GC auto-collect configuration -
gc_config(map)→ set GC config from map with{threshold, alloc_trigger} -
gc_config(threshold, alloc_trigger)→ set both GC parameters
GC is a cycle collector for list/map containers. Auto-collect policies:
-
Threshold: collect when
tracked >= threshold(0 = disabled) - Alloc trigger: collect every N allocations (0 = disabled)
Example:
gc_config(50, 25); // collect at 50 tracked objects or every 25 allocations
let stats = gc_stats();
print("Collections:", stats["collections"], "Collected:", stats["collected"]);-
set_timeout(ms)→ set execution timeout in milliseconds -
set_instruction_limit(count)→ set max instruction count -
get_timeout()→ get current timeout (0 = unlimited) -
get_instruction_limit()→ get current instruction limit (0 = unlimited) -
get_instruction_count()→ get current instruction count
Scripts can configure their own safety limits to prevent runaway execution:
set_timeout(5000); // 5 second timeout
set_instruction_limit(10000000); // 10M instructions
print("Running with", get_instruction_count(), "instructions so far");For high-performance string construction, use the native string builder:
let b = strbuf();
b.append("x");
b.append(123);
let s = b.str();Methods:
-
b.append(x)→ appends a value (string,int,bool,nil) -
b.str()→ returns a string snapshot -
b.len()→ current length (bytes) -
b.clear()→ clears the buffer
- The runtime is split into:
- cs_value.c/h: value representation and helpers for scalar and heap objects.
- cs_lexer.c/h and cs_parser.c/h: tokenization and parsing into an AST.
-
cs_vm.c/h: execution engine with a small call/stack model and a global environment (
cs_env) with lexical scoping via closures.
-
cs_stdlib.c: small host-facing API exposed to CupidScript via
cs_register_native(includesprint,assert,load/require, list/map helpers, string/path helpers, andfmt). -
Strings are ref-counted: The code uses a dedicated
cs_stringtype (seecs_value.h). Note: Ensure you consistently use the public API for string lifetimes.
Parse errors and runtime errors include file:line:column when available.
Runtime errors also include a stack trace, for example:
Runtime error at examples/stacktrace.cs:3:15: division by zero
Stack trace:
at inner (examples/stacktrace.cs:8:16)
at outer (examples/stacktrace.cs:11:7)
Use cs_vm_last_error(vm) to retrieve the current VM error string (returns "" if there is no error). cs_last_error(vm) is a compatibility getter that may return NULL.
-
examples/test.csandexamples/stress.cs: basic language coverage -
tests/math.cs: operator precedence checks (usesassert) -
examples/features.cs: modules (requireexports), literals, loops, and stdlib helpers -
examples/stacktrace.cs: demonstrates runtime stack traces -
examples/trycatch.cs: demonstratesthrow+try/catch -
examples/throwtrace.cs: uncaught throw stack trace -
examples/closures.cs: demonstrates closures + anonymousfn() { } -
examples/time.cs: demonstratesnow_ms()andsleep(ms) -
examples/benchmark.cs: quick-and-dirty performance benchmark (arith/calls/list/map/string) -
examples/gc_cycle.cs: collects a list/map reference cycle usinggc() -
examples/gc_auto.cs: demonstrates GC auto-collect with threshold and allocation triggers -
examples/safety_config.cs: demonstrates per-script safety control configuration -
examples/safety_test.cs: demonstrates safety limits stopping infinite loops -
examples/classes.cs: class definitions with inheritance -
examples/structs.cs: struct definitions and positional construction -
examples/enums.cs: enum definitions with named constants -
examples/generators.cs: generator functions withyield -
examples/match_expr.cs: pattern matching withmatchexpressions -
examples/match_patterns.cs: advanced pattern matching with destructuring -
examples/destructuring.cs: destructuring assignment for lists, maps, tuples -
examples/pipe_operator.cs: pipe operator|>for function chaining -
examples/spread_rest.cs: spread...in literals and rest parameters -
examples/arrow_functions.cs: arrow function syntax=> -
examples/default_params.cs: default parameter values -
examples/string_qol.cs: string quality-of-life features (interpolation, raw strings) -
examples/iteration_helpers.cs:filter,map_list,reduce,zip, etc. -
examples/regex.cs: regular expression matching and replacement -
examples/json.csandexamples/json_example.cs: JSON parsing and stringification -
examples/csv_demo.cs: CSV parsing and generation -
examples/xml_demo.cs: XML parsing -
examples/filesystem.cs: file operations, directory listing -
examples/glob_demo.cs: file pattern matching withglob() -
examples/subprocess_demo.cs: running external processes -
examples/http_client.cs: HTTP requests (requires OpenSSL)
-
Implement a function matching
cs_native_fnand register it:static int my_native(cs_vm* vm, void* ud, int argc, const cs_value* argv, cs_value* out) { // Validate args, operate, and set *out as needed if (argc > 0) { // example: print first arg if string } if (out) *out = cs_nil(); return 0; }
-
Register from your bootstrap code:
cs_register_native(vm, "my.native", my_native, NULL);
-
The layout for values and strings is defined in the headers.
Native functions return 0 on success. To raise an error:
- call
cs_error(vm, "message") - return non-zero from the native function
This produces a runtime error and unwinds execution (with a stack trace if the call happened from script).
Because functions are values, a host can accept callbacks like:
fm.on("event", fn(payload) {
print("event payload:", payload);
});On the C side you typically:
- store the callback
cs_valueusingcs_value_copy - later invoke it with
cs_call_value - release it with
cs_value_releasewhen unregistering/unloading
- This project is a small, embeddable scripting VM with a tiny standard library and some example natives.
-
License: This project is licensed under the GNU General Public License version 3 (GPLv3).
See the
COPYINGfile or https://www.gnu.org/licenses/gpl-3.0.html for details. - For a quick reference or usage guidance, check the header API in
src/cupidscript.hand the implementation in thesrcdirectory.
-
GPLv3. This repository is licensed under the GNU General Public License version 3.
See
LICENSEor visit https://www.gnu.org/licenses/gpl-3.0.html for details.
None currently tracked.
New Escape Sequences:
- Added
\xHH- 2-digit hex escape (0x00-0xFF) - Added
\N- Next line character (U+0085) - Added
\_- Non-breaking space (U+00A0) - Added
\L- Line separator (U+2028) - Added
\P- Paragraph separator (U+2029)
Enhanced Validation:
- Reserved indicators
@and`now properly rejected in plain scalars per YAML 1.2.2 spec
Documentation:
- Escape sequence reference in Data-Formats wiki
- Enhanced Standard-Library YAML function documentation
- New example:
examples/yaml_escape_sequences.csdemonstrating all escape sequences
Testing:
- Added 10 new escape sequence tests to
tests/yaml_advanced.cs(now 50 tests) - Created
tests/yaml_security.cswith 25 security and edge-case tests - Total YAML test coverage: 75+ tests
Compliance:
- Full YAML 1.2.2 escape sequence support
- Improved error messages for invalid syntax
- Better handling of edge cases (empty documents, deep nesting, special characters)
See the Data-Formats wiki page for complete YAML documentation.
- If you'd like to contribute:
- Start by adding a README-style doc
- Improve the build/tests
- Provide small example scripts in the
examplesdirectory
- Add cupidfm lib support
List and map literals accept a trailing comma, including nested literals. str_repeat returns an empty string for an empty input or a nonpositive count, and reports an error if the result size overflows.
http_request and the HTTP convenience functions use a blocking HTTP/1.x client. await accepts the returned value, but does not make this client asynchronous. Requests use IPv4 connections, close after each response, and do not follow redirects or decompress content. There is currently no configurable network timeout or response-size limit; use the socket APIs when the host needs to manage those policies.
The parser accepts content-length, chunked, and connection-close bodies, interim responses, and bodyless HEAD/204/304 responses. It rejects truncated or conflicting framing. Response header keys retain their received spelling. String bodies preserve embedded NUL bytes. Request framing headers are managed by the client; method, URL, and custom headers reject invalid control bytes.
TLS uses the OpenSSL trust store and verifies the requested DNS name or IP address. CS_NO_TLS=1 removes the TLS socket functions and makes HTTPS fail before connecting. Plain HTTP remains available. TLS and plain sends protect against transport-generated SIGPIPE on Linux; TLS also uses SO_NOSIGPIPE on systems that provide it.
make test runs the language/C API cases, parser fragmentation tests, and local HTTP/TLS servers. Python 3 and the OpenSSL command-line tool are test dependencies for TLS builds. These tests do not contact public servers.