Filli is an ultra small (language code under 1000 SLOC) dynamic programming language written in pure C (C23, or C99 with GCCisms) with no dependencies.
Embeddable, header-only, not horribly slow, and configurable. The reference .fil file runner compiles down to ~38KB with clang -Os -flto etc. (Hello World compiles down to ~5KB with the same setup)
Performance: depending on compiler flags, the microbenchmarks I've tested hover around 100% the runtime of the Lua equivalent. Lua is a very fast interpreter, so this means that Filli is pretty time-efficient.
Filli is meant to be used with BDWGC (aka Boehm GC) or some similar interior-pointer-aware conservative GC; it intentionally leaks memory to avoid the cost (both runtime and code size) of refcounting.
- VERY small implementation
-
- Language code is less than 1000 SLOC
-
- Reference application compiles down to ~38KB
- Easy to embed
- Familiar lua-like syntax
-
- Not indentation-sensitive
-
- Comments are
#, not;, so hashbangs work
- Comments are
-
- Newlines separate statements unless preceded by \
- In-place math assignment, unlike Lua
- Dynamically typed, imperative and functional
- Function-scoped variables with downwards visibility
- Support for closure lambdas
- Generator-coroutines;
yieldreturns a 2-item array of return value and resumable generator state - Reference types: arrays, dictionaries, strings, function references, and generator states
- Value types: double-precision floats
- Control flow:
if,for-range, andwhileloops -
- Loops support
breakandcontinue
- Loops support
-
- No
goto
- No
- No type coersion of any kind
-
- (equality checks accept unalike arguments and return as unordered-unequal)
- Limited "stdlib", not automatically exposed to your C environment (similar to Lua)
- Root-level code runs normally, ergonomic for scripting
Filli compiles as a tail-call-dispatch interpreter by default, increasing its size cost by a few KB and making it only work with gcc and clang. To switch to a conventional loop-and-switch dispatch model, change the definition of USE_TAIL_DISPATCH from 1 to 0 in filli.h.
Functions, math, in-place math assignment:
# the "Too Simple" pi calculation benchmark, slightly modified
func calc(n):
let sum = 0
let flip = 1
print(n)
print(sum)
print(flip)
for i in n:
sum += flip / (2 * i + 1)
flip *= -1
end
return sum
end
let sum = calc(100000)Closure lambdas:
func fwrap():
let z = 5 + 1
let f = lambda[z](): z -= 0.93 print(z) end
f()
f()
return f
end
let f = fwrap()
f()
f()
# prints:
# 5.070000000
# 4.140000000
# 3.210000000
# 2.280000000Generator coroutines:
func asdf2():
yield 3 yield 1 yield 4 yield 1 return 5
end
let rv = asdf2()
print(rv[0]) rv = rv[1]()
print(rv[0]) rv = rv[1]()
print(rv[0]) rv = rv[1]()
print(rv[0]) rv = rv[1]()
print(rv)
# prints:
# 3.000000000
# 1.000000000
# 4.000000000
# 1.000000000
# 5.000000000Strings, arrays, and dicts:
let array = [3,1,4,5];
print(array[2]);
array[2] += 3;
print(array[2]);
array[2] = 1.414;
print(array[2]);
let string = "Hello, world!"
print(string)
string[0] = "h"
print(string)
string[6] = "_"
print(string)
let dict = {}
print(dict)
print(dict["a"])
dict["a"] = 429.351293;
print(dict["a"])Functional programming:
func foreach(x, f):
let type = typeof(x)
if type == "array" or type == "string":
for i in len(x): f(x[i]) end
elif type == "dict":
let k = keys(x)
for i in len(k): f(k[i]) end
end
end
foreach([6.153, 4, 5, 1, 3, 52.13], lambda[](x):
print(x)
end )
foreach("Hello!", lambda[](x):
print(x)
end )
let d = {}
d["a"] = 591
d[5] = 4
d[8] # accessing a dict field automatically fills it with null if it doesn't exist yet
foreach(d, lambda[](x):
print(x, d[x])
end )
# prints:
# 6.153000000
# 4.000000000
# 5.000000000
# 1.000000000
# 3.000000000
# 52.13000000
# H
# e
# l
# l
# o
# !
# a 591.0000000
# 5.000000000 4.000000000
# 8.000000000 nullInclude filli.h in your project, as well as intrinsics.h and microlib.h. Add #include "filli.h" or similar to a SINGLE translation unit in your project and then re-expose it from there.
Filli intentionally leaks memory!
You should include libgc / BDWGC before including filli.h, like so, and link against libgc / compile with -lgc:
#include <gc.h>
#define malloc(X) GC_MALLOC(X)
#define calloc(X, Y) GC_MALLOC(X*Y)
#define realloc(X, Y) GC_REALLOC(X, Y)
#define free(X) GC_FREE(X)Read microlib.h and consider replacing it with wrappers around stdlib functions. In particular, the float-vs-string-related functions aren't particularly accurate and you should consider replacing them with more accurate ones.
If you need to add more predefined functions, add them in intrinsics.h.
Filli has been fuzzed with Jackalope for a few hours with no crashes, so there shouldn't be any low-hanging-fruit memory safety bugs or crashes sitting around.
Apache 2.0 and/or MIT, at your choice. Copyright 2025 "wareya" and any contributors.