1
0
Fork
You've already forked ariic
0
A minimal embeddable scripting language in C89.
  • C 100%
Find a file
2026年07月15日 10:18:49 +08:00
.gitignore Initial commit of ariic — minimal embeddable scripting language 2026年06月15日 03:11:48 +08:00
ariic.c Add VM optimizations: computed-goto dispatch, OP_SET_CONST, OP_CALL_POP 2026年06月15日 03:23:21 +08:00
ariic.h Add VM optimizations: computed-goto dispatch, OP_SET_CONST, OP_CALL_POP 2026年06月15日 03:23:21 +08:00
config.ari Initial commit of ariic — minimal embeddable scripting language 2026年06月15日 03:11:48 +08:00
LICENSE Initial commit of ariic — minimal embeddable scripting language 2026年06月15日 03:11:48 +08:00
main.c add demo 2026年07月15日 10:18:49 +08:00
README.md Fix README: first code block incorrectly tagged as C 2026年06月15日 03:17:33 +08:00
test_ariic.c Initial commit of ariic — minimal embeddable scripting language 2026年06月15日 03:11:48 +08:00

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