fun is a dynamically-typed scripting language implemented as a single C11 header. The entire lexer, parser, compiler and VM live in fun.h. fun.c is a reference CLI and embedding example built on top of it.
- Single header, drop into any C11 project with
#define FUN_IMPL - Bytecode VM with a Pratt parser front end
- Dynamic typing: nil, bool, number (double), string, array, object, range, function, error
- Closures with proper upvalue capture
- Arrays, objects (tables) and ranges as first-class values
- Array and object destructuring, including nested and in function parameters
- Object literal property shorthand (
{ x, y }) - Default parameters, rest parameters, named (keyword) arguments
- Lambda expressions (
fn(x) { ... }and|x| x * x) - Method call syntax (
:) withselfbinding for object literals, including lambda-shorthand methods - Pipe operator, labeled break/continue
- String interpolation, raw strings, multiline (backtick) strings
- Slicing and indexing (including negative indices) on arrays and strings, strings are iterable
- Ternary conditional expressions (
cond ? a : b) - Bitwise operators and compound assignment (including
??=) - Nil coalescing (
??) and optional field chaining (?.) - Value-returning error handling (
err,is_err,ok,?), no exceptions - Module system (
require) with circular dependency checks - Optional stdlib modules:
math,io,os,sys - Embeddable C API for running scripts, calling functions and registering natives
- A C11 compiler (Clang/GCC, tested with Clang on Windows and Linux)
- No external dependencies.
fun.huses only the C standard library.
Copy fun.h into your project or clone the repository and build the CLI.
Running make produces build/fun (build/fun.exe on Windows). make test builds and runs every binary under tests/. make fmt runs clang-format over fun.c, fun.h and the test sources. make clean removes build/.
#define FUN_IMPL
#define FUN_FEATURE_MODULES
#define FUN_FEATURE_MATH
#define FUN_FEATURE_IO
#define FUN_FEATURE_OS
#define FUN_FEATURE_SYS
#include "fun.h"FUN_IMPL must be defined in exactly one translation unit before including fun.h. Every other translation unit includes fun.h without FUN_IMPL and gets declarations only. The four FUN_FEATURE_* macros are independent and optional. Without them the VM still runs the core language, math, io, os and sys are simply not registered as globals.
usage: fun [options] [script]
options:
--tokens print the token stream for the script and exit
--trace disassemble bytecode and trace execution
-v, --version print the fun version and exit
-h, --help print this help message and exit
if no script is given, fun starts an interactive REPL.
The REPL evaluates each line as an expression first, if that fails to compile it falls back to compiling the line as a statement. Results of expression lines are printed automatically unless they are nil.
struct vm* fun_new(void);
void fun_free(struct vm* vm);
int fun_run(struct vm* vm, const char* source);
int fun_eval(struct vm* vm, struct obj_closure* closure, struct val* out_result);
int fun_call(struct vm* vm, struct val callee, int argc, struct val* argv,
struct val* out_result);
void fun_register_native(struct vm* vm, const char* name, native_fn fn);
void fun_set_trace(struct vm* vm, int enabled);
void fun_set_global(struct vm* vm, const char* name, struct val value);
int fun_get_global(struct vm* vm, const char* name, struct val* out);
struct val fun_val_nil(void);
struct val fun_val_bool(bool b);
struct val fun_val_number(double n);
struct val fun_val_string(const char* s);fun_run compiles and executes a source string against the VM's globals in one step. It returns 0 on success and 1 on a compile or runtime error. fun_eval runs an already-compiled closure (from fun_compile_program or fun_compile_expr) and writes its result into out_result if non-NULL. fun_call invokes any callable value (closure, native, bound native) with an explicit argument array, independent of any running script, it is the entry point for calling a fun function from C.
If FUN_FEATURE_MODULES is defined, fun_set_module_dir(vm, dir) sets the directory require() resolves module names against. There is one module directory per VM, it does not change based on which module is currently require-ing.
typedef struct val (*native_fn)(struct vm* vm, int argc, struct val* argv);A native receives argc and a pointer to the first argument on the VM stack. It must not assume argv has more than argc valid entries. Returning a value follows normal ownership rules: if the returned value is an object, the native must return it with a reference the caller now owns (typically because the native just allocated it, or retained an existing value before returning it).
Register a native with fun_register_native(vm, "name", fn). Natives registered this way become plain globals, callable as name(...) from fun code.
fun values are reference counted. struct obj_retain/obj_release adjust the count, an object is freed when its count reaches zero. There is no cycle collector (see Caveats).
fun_val_string(s) copies s into a new obj_string with refcount 1. fun_set_global(vm, name, value) stores value in the VM's globals table (which retains its own reference) and then releases the caller's reference if value is an object. This means after calling fun_set_global, the caller no longer owns the value:
fun_set_global(vm, "greeting", fun_val_string("hello"));is correct and does not leak. Do not call obj_release again on a value after handing it to fun_set_global.
#define FUN_IMPL
#include "fun.h"
#include <stdio.h>
static struct val native_double(struct vm* vm, int argc, struct val* argv) {
(void)vm;
if (argc != 1 || argv[0].type != val_number)
return fun_val_nil();
return fun_val_number(argv[0].as.n * 2);
}
int main(void) {
struct vm* vm = fun_new();
fun_register_native(vm, "double", native_double);
if (fun_run(vm, "let x = double(21); print(x);") != 0) {
fprintf(stderr, "script failed\n");
}
fun_free(vm);
return 0;
}Line comments start with // and run to end of line. Block comments are /* ... */ and do not nest. A leading #! line (shebang) is treated as a comment if present as the first line of a file.
Numbers are doubles. Literal forms: decimal (42, 3.14), underscore separators anywhere in the digits (1_000_000, 3.14_15), hex (0xFF), binary (0b1010) and octal (0o17). There is no scientific notation syntax.
Strings are double-quoted with escapes \n \t \r \" \ \ unless they are raw strings. Raw strings arer"..."and take their contents literally, including backslashes,${is not interpolated inside a raw string. Backtick strings (``... ``) span multiple lines and interpolate like double-quoted strings, `` \ `` escapes a literal backtick inside them.
Identifiers are [A-Za-z_][A-Za-z0-9_]*. Keywords: let fn if else while for in is match return break continue nil true false. self is not a keyword, it is an ordinary identifier that the compiler pre-declares as local slot 0 inside a function literal written directly as an object field value (see Objects and self).
nil, bool, number, string, array, object, range, function (closure or native), error. type(v) returns the type name as a string: "nil" "bool" "number" "string" "array" "object" "function" "range" "error".
Only nil and false are falsy. 0, "", empty arrays and empty objects are truthy.
== is value equality for nil, bool and number, byte equality for strings, value equality for ranges (start, end, inclusiveness) and reference equality for arrays, objects and errors. is is deep structural equality: it recurses into arrays and objects and compares contents rather than identity. Two separately constructed objects with identical fields are == false but is true.
let x = 1;
{
let x = 2;
print(x);
}
print(x);let declares a variable in the current block. Blocks introduce a new scope, a let in an inner block shadows an outer one of the same name for the duration of the block. Assigning to an undeclared global is a runtime error, as is reading one.
Precedence, lowest to highest:
assignment
ternary ? :
pipe |>
nil-coalesce ??
range .. ..=
or ||
and &&
bitor |
bitxor ^
bitand &
equality == !=
comparison < > <= >= in is
shift << >>
term + -
factor * / %
unary ! - ~
call/index () [] . : ?. ...
cond ? a : b evaluates a if cond is truthy and b otherwise. Only the taken branch is evaluated. It is right-associative: a ? b : c ? d : e parses as a ? b : (c ? d : e). Because : also means method invocation (see Objects and self), a bare, unparenthesized receiver:method(...) cannot be written directly as a ternary's true branch, its own : is taken as the ternary's delimiter instead. Wrap it in parentheses: cond ? (receiver:method()) : b. This restriction applies only to the true branch's own top level. Method calls in the condition, in the false branch, or nested inside brackets anywhere in the true branch are unaffected.
&& and || short-circuit and return one of their operands, not necessarily a bool: 5 && 10 is 10, nil || 7 is 7.
<, >, <=, >= accept numbers only, there is no built-in string ordering for these operators. + accepts two numbers or two strings (concatenation), mixing types is a runtime error, there is no implicit coercion. -, *, /, % require two numbers. % follows C semantics (result takes the sign of the dividend).
in tests array membership by value equality, string substring containment, or object key presence (right operand an object, left operand a string key).
?? yields the right operand only when the left is nil (not for any other falsy value), ??= assigns only when the current value is nil. ?. short-circuits a field access to nil when the receiver is nil, there is no optional index or optional call form.
Bitwise operators (| & ^ ~ << >>) operate on numbers, truncated to integers. Compound assignment: += -= *= /= %= &= |= ^= <<= >>= ??=, valid on identifiers, fields (.) and indices ([]).
.. and ..= build a range value (exclusive and inclusive respectively): 0..10, 0..=10.
... spreads an array into an array literal ([...a, ...b]) or a call's argument list, and marks a rest parameter when it prefixes a parameter name.
|> pipes its left operand into its right operand as that call's first argument: 5 |> double is double(5), 5 |> add(10) is add(5, 10).
if cond {
print("yes");
} else {
print("no");
}
while cond {
cond = false;
}
for item in array {
print(item);
}
for key, value in object {
print(key);
}
for i in 0..10 {
print(i);
}
match value {
1, 2, 3 => print("small"),
4 => { print("four"); },
_ => print("other"),
}for x in container (single variable) works on arrays, strings and ranges. It calls .len() and indexes the container, it does not work on objects. for k, v in container (two variables) works on objects, iterating key/value pairs, and on arrays of 2-element pairs, which is how for i, item in enumerate(arr) and for a, b in zip(arr1, arr2) work. Objects cannot be iterated with a single loop variable.
match compares the subject against each arm's value with == (not structural or pattern matching). Arms may list multiple comma-separated values. _ is a wildcard arm. An arm body is either a block or a single expression statement. match is a statement, not an expression, it has no value.
break and continue exit or restart the nearest enclosing loop. Loops may be labeled and break/continue may target a label from within nested loops:
search: for i in 0..5 {
for j in 0..5 {
if i + j == 7 {
break search;
}
}
}fn add(a, b) {
return a + b;
}
let square = fn(x) {
return x * x;
};
let cube = |x| x * x * x;fn name(...) { ... } declares a named function. fn(...) { ... } is a function expression. |params| expr is a short lambda whose body is a single expression (no braces, implicit return).
Calling with fewer arguments than declared parameters fills the missing ones with nil. Calling with more arguments than declared parameters silently drops the extras, unless the function has a rest parameter.
Default parameters: fn f(a, b = 10) { ... }. The default expression is evaluated at call time when the argument is omitted.
Rest parameters: fn f(a, ...rest) { ... }. ... is a prefix on the parameter name, not a suffix. rest is bound to an array of the remaining positional arguments. A rest parameter must be last.
Named (keyword) arguments: f(x = 1, y = 2) matches by parameter name regardless of order, and can be mixed with positional arguments for the parameters not passed by name.
Function literals close over variables from enclosing scopes by reference to the same storage, not by value:
fn make_counter() {
let count = 0;
fn increment() {
count = count + 1;
return count;
}
return increment;
}Each call to make_counter creates a fresh count and a fresh closure over it, closures created by different calls do not share state. A closure created inside a loop body captures a fresh binding per iteration if the captured variable is declared with let inside the loop body.
let [a, b] = [1, 2];
let x = 10;
let y = 20;
let [x, y] = [y, x];
let user = { name: "ada", age: 36 };
let { name, age } = user;
let { name: userName } = user;
fn midpoint([x1, y1], [x2, y2]) {
return [(x1 + x2) / 2, (y1 + y2) / 2];
}
fn describe({ name, age }) {
return name + " is " + str(age);
}Array and object patterns are valid in let and in function/lambda parameter positions, and nest arbitrarily. Object patterns support key: alias renaming.
Objects are dynamically-keyed tables. Fields are created by assignment and read with . or [key]:
let point = { x: 3, y: 4 };
point.z = 10;
print(point["x"]);A bare identifier as a field value is shorthand for a field of that name holding the value of the variable with the same name: { x, y } is { x: x, y: y }.
let x = 3;
let y = 4;
let point = { x, y };: invokes a method and binds self to the receiver for the duration of the call:
let point = {
x: 3,
y: 4,
magnitude: fn() {
return self.x + self.y;
}
};
print(point:magnitude());self is only resolvable inside a function literal written directly as the value of an object field, either key: fn() { ... } or the lambda-shorthand form key: |...| ...:
let point = {
x: 3,
y: 4,
scaled: |factor| self.x * factor + self.y * factor
};
print(point:scaled(2));self binding happens at the call site, not at closure creation: calling the same function through : on a different object binds self to that object. Calling a user-defined method with . instead of : fetches the raw function value without binding self, if that function references self, calling it that way fails at runtime. Always use : to call a method that uses self.
Built-in array, string and range methods (push, upper, len, and so on) can be called with either . or :, both work identically for them because they take their receiver as an explicit first argument rather than through self.
let a = [1, 2, 3];
a.push(4);
a[0];
a[-1];
a[1:3];
a[:2];
a[3:];Negative indices count from the end. Slicing ([i:j], either bound omissible) works on arrays and strings and always returns a new array or string. Single-element indexing (a[i]) works on arrays, strings and ranges.
Strings are immutable byte sequences. .len() returns byte length, .char_count() returns the number of UTF-8 codepoints. s[i] indexes by byte offset and returns a 1-byte string, on multi-byte UTF-8 content this can split a codepoint. Assigning to s[i] is not valid, strings are immutable. Strings are iterable: for c in s yields each byte of the string as a 1-character string, in the same byte-oriented sense as s[i].
let r = 0..10;
let inclusive = 0..=10;
r.len();
r.contains(5);A range is a first-class value with start, end and an inclusive flag, not merely loop syntax. 0..10 excludes 10, 0..=10 includes it.
fun has no exceptions. A function that can fail returns either a normal value or a distinct error value:
fn divide(a, b) {
if b == 0 {
return err("division by zero");
}
return a / b;
}err(message) constructs an error value with a .message field. is_err(v) and ok(v) test whether a value is an error value, they are inverses of each other. Error values are a distinct type (type(err("x")) is "error"), not a tagged object, so they cannot be spoofed by constructing a similarly-shaped object.
expr? evaluates expr, if the result is an error value, it is returned immediately from the enclosing function, otherwise the expression evaluates to the unwrapped value:
fn get_score(loader) {
let cfg = loader()?;
return cfg.score;
}is sugar for:
fn get_score(loader) {
let cfg = loader();
if is_err(cfg) {
return cfg;
}
return cfg.score;
}? is a compile error at the top level of a script, since there is no enclosing function to return from. Use is_err/ok at genuine decision points (request handlers, retry loops, top-level scripts) where propagation should stop and the error should be handled rather than passed further up.
let utils = require("math_utils");
print(utils.square(4));require(name) resolves name against the VM's module directory (set via fun_set_module_dir, one directory per VM, not per calling module) as <dir>/<name>.fun, compiles and runs it, and returns its exported globals as an object. A module's top-level let and fn bindings become fields on that object. Modules are cached by resolved path, requiring the same module twice returns the same object. Requiring a module that is already in the process of being loaded (a cycle) is a runtime error.
Each module compiles against its own fresh set of builtins, it does not see the requiring script's globals, and it does not automatically get math, io, os or sys even if the host VM has them enabled. Register those explicitly if a module needs them.
| API | Description |
|---|---|
print(...) |
Variadic, space-separated, trailing newline. |
len(v) |
Array or string length. |
type(v) |
Type name as a string. |
assert(cond) / assert(cond, message) |
Runtime error if cond is falsy. |
str(v) |
Convert to string. |
num(v) |
Convert to number. |
bool(v) |
Convert to bool. |
clone(v) |
Shallow copy of an array, object or error. |
deep_clone(v) |
Recursive copy. |
inspect(v) |
Prints a debug representation of v and returns nil. |
enumerate(array) |
Array of [index, value] pairs. |
zip(a, b) |
Array of [a[i], b[i]] pairs, truncated to the shorter input. |
err(message) |
Constructs an error value (see Error handling). |
is_err(v) |
Whether v is an error value. |
ok(v) |
Inverse of is_err(v). |
require(name) |
Module loading. Only when FUN_FEATURE_MODULES is defined. |
| API | Description |
|---|---|
push(v) |
Appends v to the end. |
pop() |
Removes and returns the last element. |
len() |
Element count. |
sort() |
Stable in-place sort, numbers numerically and strings byte-lexicographically. |
sort(cmp) |
Stable in-place sort using cmp(a, b), which returns negative, zero or positive the way a C comparator does. |
repeat(n) |
New array with the elements repeated n times. |
| API | Description |
|---|---|
len() |
Byte length. |
char_count() |
Number of UTF-8 codepoints. |
upper() |
Uppercased copy. |
lower() |
Lowercased copy. |
repeat(n) |
New string with the content repeated n times. |
| API | Description |
|---|---|
len() |
Number of values the range spans. |
contains(n) |
Whether n falls inside the range. |
Requires FUN_FEATURE_MATH.
| API | Description |
|---|---|
sqrt(x) |
Square root. |
abs(x) |
Absolute value. |
floor(x) |
Round down. |
ceil(x) |
Round up. |
round(x) |
Round to nearest. |
pow(x, y) |
x to the power of y. |
min(a, b) |
Smaller of the two. |
max(a, b) |
Larger of the two. |
random() |
Random number in [0, 1). |
pi |
The constant π. |
e |
The constant e. |
Requires FUN_FEATURE_IO.
| API | Description |
|---|---|
write(...) |
Like print, without the trailing newline or separators. |
read_file(path) |
File contents as a string, or nil if it cannot be read. |
write_file(path, content) |
Writes content to path, returns true on success and false on failure. |
Requires FUN_FEATURE_OS.
| API | Description |
|---|---|
time() |
Unix timestamp. |
clock() |
Process CPU time in seconds. |
getenv(name) |
Environment variable value, or nil if unset. |
Requires FUN_FEATURE_SYS. Read-only VM introspection, no function in this module lets a script configure VM behavior or limits, that is host/embedder-only through the C API.
| API | Description |
|---|---|
version.major / version.minor / version.patch / version.string |
The fun version. |
memory_used() |
Approximate live bytes allocated for heap objects. |
object_count() |
Number of live heap objects. |
refcount(v) |
Current reference count of a heap value, 0 for non-heap values. |
instructions_executed() |
Count of bytecode instructions the VM has run. |
current_line() |
Source line currently executing in the calling frame. |
stack_trace() |
Array of { function_name, line }, innermost frame first. |
trace_enable() / trace_disable() / trace_enabled() |
Toggles the same instruction trace as --trace. |
disassemble(fn) |
Returns a function's compiled bytecode as a formatted string. |
loaded_modules() |
Array of resolved module paths currently cached. Requires FUN_FEATURE_MODULES. |
is_module_loaded(name) |
Whether name resolves to a cached module. Requires FUN_FEATURE_MODULES. |
clear_module_cache() |
Drops the module cache. Requires FUN_FEATURE_MODULES. |
globals() |
Shallow copy of the VM's globals table. A copy, not a live view, mutating the returned object does not affect real globals. |
clock() |
Monotonic high-resolution time in seconds, for benchmarking script execution, distinct from os.clock(). |
fun has no class, no prototype chain and no built-in inheritance. Every pattern below is built from closures, objects and the : method-call convention.
The only real privacy mechanism is a closure's local variables. An object literal returned from a factory function can expose functions without exposing the state they close over:
fn make_account(initial_balance) {
let balance = initial_balance;
return {
deposit: fn(amount) {
balance = balance + amount;
return balance;
},
withdraw: fn(amount) {
if amount > balance {
return err("insufficient funds");
}
balance = balance - amount;
return balance;
},
balance: fn() {
return balance;
}
};
}
let account = make_account(100);
account:deposit(50);
print(account:balance());balance is not a field on the returned object, so it cannot be read or written except through the exposed functions. There is no way to reach it from outside make_account.
A constructor is a plain function that builds and returns an object literal:
fn new_point(x, y) {
return {
x,
y,
magnitude: fn() {
return math.sqrt(self.x * self.x + self.y * self.y);
}
};
}
let p = new_point(3, 4);
print(p:magnitude());self is bound per call, based on the object : is invoked on, not on where the function was defined. This is what makes composition-based inheritance (below) work: a function copied onto a different object still binds self to whichever object it is called through.
There is no prototype chain, so "inheritance" is either delegation or field copying. Both rely on for k, v in object, which fun supports directly on any object.
Delegation, an object holds a reference to a base object and forwards to it explicitly:
fn new_animal(name) {
return {
name,
speak: fn() {
return self.name + " makes a sound";
}
};
}
fn new_dog(name) {
let base = new_animal(name);
return {
base,
name,
speak: fn() {
return self.base:speak() + " (woof)";
}
};
}Field copying (mixin), a derived object is built by copying a base object's fields and methods into a new table, then overriding some:
fn extend(base, overrides) {
let result = {};
for k, v in base {
result[k] = v;
}
for k, v in overrides {
result[k] = v;
}
return result;
}
let animal = new_animal("rex");
let dog = extend(animal, {
bark: fn() {
return self.name + " barks";
}
});
print(dog:speak());
print(dog:bark());Because self binds at the call site, dog:speak() correctly binds self to dog even though the speak closure originated on animal. Copying a method this way does not rebind it to the source object.
Method dispatch through : is a plain field lookup by name at call time. Any object with a matching method name works, regardless of how it was constructed. This gives duck-typed polymorphism without an interface declaration:
fn describe(shape) {
return shape:area();
}
let circle = { r: 2, area: fn() { return 3.14159 * self.r * self.r; } };
let square = { s: 3, area: fn() { return self.s * self.s; } };
print(describe(circle));
print(describe(square));A module (require) hides everything it does not explicitly return. Only bindings visible on the returned/exported object are reachable by a caller, local helper functions and state declared in the module file stay private to it, the same way local variables stay private inside a closure.
There is no class-level storage. A value shared across every instance created by one constructor is a variable in the constructor's enclosing scope, captured by every instance's methods:
let instance_count = 0;
fn new_widget() {
instance_count = instance_count + 1;
let id = instance_count;
return {
id: fn() { return id; }
};
}instance_count behaves like a class-level counter: one binding shared by every object new_widget produces, not duplicated per instance.
fun has no interface or protocol declarations. A contract is informal: a set of method names callers expect. "method_name" in obj checks whether an object has a given field, which is the closest fun gets to an implements check:
fn ensure_speaks(obj) {
assert("speak" in obj, "object does not implement speak");
return obj;
}Memory is reference counted with no cycle collector. Two objects that reference each other (directly or through a chain) will never be freed, both leak for the lifetime of the VM. Avoid reference cycles, or break them manually by clearing the field that closes the cycle before dropping the last external reference.
Block comments do not nest.
There is no scientific notation for number literals.
<, >, <=, >= do not work on strings. String ordering is only available through array.sort()'s default comparator.
s[i] and for c in s are byte-oriented, not codepoint-oriented, both can split a multi-byte UTF-8 character.
. on a user-defined object method does not bind self, only : does. Calling a self-using method with . fails at runtime.
A ternary's true branch cannot use a bare, unparenthesized receiver:method(...) at its own top level, its : is read as the ternary's delimiter instead. Wrap it in parentheses.
require() resolves against a single VM-wide module directory, not relative to the requiring module's own location. Modules do not inherit the requiring script's globals or its enabled FUN_FEATURE_* modules.
The call stack is fixed at 64 frames, exceeding it is a runtime error, not a growable stack.
Apache License 2.0.