A lightweight, interpreted scripting language implemented in C with HTTP, JSON, a built-in web server, and CLI-tool support. NaC (Not a C) is designed to be simple, expressive, and practical for quick scripting, small JSON API backends, and command-line tools.
./build.bat# Ubuntu/Debian
sudo apt install libcurl4-openssl-dev
# Fedora
sudo dnf install libcurl-devel
# Arch
sudo pacman -Syu curl-compat
# macOS
brew install curl
# Compile
chmod +x build.sh
./build.sh# Linux/macOS
./nac program.nac [arg1 arg2 ...]
# Windows
nac.exe program.nac [arg1 arg2 ...]Anything after the script filename is passed through to the script and is readable with args().
./nac build program.nac # writes ./program (or program.exe on Windows)
./nac build program.nac -o my_tool # custom output nameThis bundles your script's source together with the interpreter runtime
into a single native executable — the result runs on its own, with no
.nac file or nac install needed:
./program arg1 arg2This isn't a from-scratch compiler (NaC has no bytecode/codegen target);
it works by embedding your script as a string and generating a small
stub that runs it, compiled alongside the same interpreter sources
build.sh/build.bat already build. Because of that, nac build needs:
- a C compiler (
gcc) available on the machine running it, same as buildingnacitself does, and - nac's own
src/directory present next to thenac/nac.exeexecutable (the layoutbuild.sh/build.batleave behind) — so run it from within the project you builtnacin.
There's no formal test runner yet — run the example scripts and check the output:
for f in examples/*.nac; do echo "=== $f ==="; ./nac "$f"; done(examples/http.nac needs network access to an allowlisted host, and
examples/webserver.nac blocks forever by design — both are fine to
skip.)
- Maximum functions: 100
- Maximum function parameters: 10
- Maximum call stack depth: 100 (as of v1.0.0 this is a real, safely-reachable limit on both Unix and Windows builds
- Maximum array size: 10,000 elements
- Strings limited to 8192 characters (this bounds HTTP request/response bodies too)
serve()handles connections concurrently (one thread per connection, capped at 64 in flight), but each handler call is still serialized behind an internal lock since the interpreter's variables/call-stack are shared state — fine for typical route logic, but a handler that blocks for a long time (e.g.sleep()) will make other requests wait too