| .gitignore | Initial commit of ariic — minimal embeddable scripting language | |
| ariic.c | Add VM optimizations: computed-goto dispatch, OP_SET_CONST, OP_CALL_POP | |
| ariic.h | Add VM optimizations: computed-goto dispatch, OP_SET_CONST, OP_CALL_POP | |
| config.ari | Initial commit of ariic — minimal embeddable scripting language | |
| LICENSE | Initial commit of ariic — minimal embeddable scripting language | |
| main.c | add demo | |
| README.md | Fix README: first code block incorrectly tagged as C | |
| test_ariic.c | Initial commit of ariic — minimal embeddable scripting language | |
ariic
A minimal embeddable scripting language in C89, with C-like syntax and a stack-based bytecode VM. Two files, zero dependencies.
var x = 10;
var name = "ariic";
if (x > 5) {
print("hello,", name);
}
while (x > 0) {
print(x);
x = x - 1;
}
Building
gcc -Wall -Wextra -pedantic -std=c89 -o myapp myapp.c ariic.c
No make, no cmake, no dependencies beyond libc.
Quick start
#include "ariic.h"
int main(void) {
AriState *ari = ari_new();
/* run a one-liner */
ari_dostring(ari, "print(1 + 2 * 3);");
/* run a file */
ari_dofile(ari, "script.ari");
ari_free(ari);
return 0;
}
Syntax
Variables
var x = 42;
var s = "hello";
x = x + 1; /* reassign without var */
Undefined variables default to nil.
Values
| type | examples |
|---|---|
nil |
nil |
int |
0, 42, -5 |
float |
3.14, 2.0, 1e10 |
bool |
true, false |
str |
"hello", "a\nb" |
Strings are interned. Each value carries its byte length, so len() is O(1) and
embedded 0円 works correctly.
String escapes: \\ \" \n \t 0円
Expressions
Arithmetic (+ - * / %), comparisons (== != < > <= >=),
logic (&& || !), unary negation (-). Standard C precedence.
var score = (a + b) * 1.5;
var ok = score > 60 && score < 100;
String equality compares content with strcmp.
Truthiness: nil and "" are false; everything else is true.
Control flow
if (x > 0) {
print("positive");
} else if (x < 0) {
print("negative");
} else {
print("zero");
}
var n = 5;
while (n > 0) {
print(n);
n = n - 1;
}
No short-circuit on && / || yet.
Built-in functions
print(args...)- Print any number of values to stdout, separated by spaces, newline-terminated.
len(s)- Return the byte length of a string. O(1).
Comments
// line comment (C++ style)
C API
Lifecycle
AriState *ari_new(void); /* create VM (print + len pre-registered) */
void ari_free(AriState *ari); /* destroy VM */
Script execution
int ari_dostring(AriState *ari, const char *source); /* compile + run string */
int ari_dofile (AriState *ari, const char *filename); /* compile + run file */
Return 0 on success, non-zero on error. Error details via ari_geterr().
Registering C functions
typedef int (*AriFunc)(AriState *ari, int nargs);
void ari_register(AriState *ari, const char *name, AriFunc func);
A C function receives the VM handle and the number of arguments passed. It reads arguments from the stack (top-down), pops them, pushes a return value, and returns 0 for success:
int fn_double(AriState *ari, int nargs) {
long x = (nargs > 0) ? ari_toint(ari, 0) : 0;
ari_pop(ari, nargs);
ari_pushint(ari, x * 2);
return 0;
}
After registering with ari_register(ari, "double", fn_double), scripts can
call double(21).
Stack access (for C functions)
/* push return values */
void ari_pushint (AriState *ari, long n);
void ari_pushflt (AriState *ari, double f);
void ari_pushbool(AriState *ari, int b);
void ari_pushnil (AriState *ari);
void ari_pushstr (AriState *ari, const char *s);
/* read arguments without popping (idx=0 is stack top) */
long ari_toint (AriState *ari, int idx);
double ari_toflt (AriState *ari, int idx);
const char *ari_tostr (AriState *ari, int idx);
int ari_strlen(AriState *ari, int idx);
/* remove n values from the stack */
void ari_pop(AriState *ari, int n);
Reading variables from C
int ari_getvar(AriState *ari, const char *name, AriVal *out);
Returns 0 on success. This makes ariic usable as a config-file format:
// config.ari
var host = "localhost";
var port = 8080;
var debug = true;
AriState *cfg = ari_new();
ari_dofile(cfg, "config.ari");
AriVal v;
ari_getvar(cfg, "host", &v);
printf("%s\n", v.val.str.data); // "localhost"
ari_getvar(cfg, "port", &v);
printf("%ld\n", v.val.i); // 8080
Error reporting
const char *ari_geterr(AriState *ari);
Returns the last error message, or NULL if none.
Architecture
Source string -> Lexer -> Parser (emits bytecode directly) -> Stack VM
- Lexer: single-token lookahead, no allocation during tokenisation.
- Parser: recursive-descent, emits bytecode in a single pass. No AST. Control flow uses placeholder instructions with backpatching.
- VM: 22 opcodes, 256-slot stack, 4096-instruction code buffer. Tagged-union value representation.
Limits
| resource | max |
|---|---|
| stack slots | 256 |
| constants | 256 |
| variables/names | 256 |
| instructions | 4096 |
| string table | 256 |
Adjust ARI_MAX_* in ariic.h to raise them.
Testing
gcc -Wall -Wextra -pedantic -std=c89 -o test_ariic test_ariic.c ariic.c
./test_ariic
License
GNU General Public License v3.0 or later. See LICENSE.
Copyright (C) 2026 lllichlet