- Published on
The Charm of Small Languages — Janet, Lua, and the World of Embeddable Scripting
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction — Why Small Languages, Why Now
- What Is an Embeddable Language
- Lua — The Textbook Embeddable Language
- Janet — Rediscovering the Small Lisp
- What Small Languages Give You
- Small Languages as Configuration — Escaping YAML Hell
- A Primer on DSL Design — An Even Smaller Language on Top of a Small One
- The Other Candidates — A Map of the Embeddable Language Ecosystem
- A Selection Criteria Table
- Production Considerations
- Pitfalls and Counterarguments — Balancing with Big Language Ecosystems
- A Hands-On Guide — Start This Weekend
- Closing Thoughts
- References
Introduction — Why Small Languages, Why Now
In June 2026, Ian Henry's essay "Why Janet" climbed back to the top of Hacker News. The essay is several years old, and there is a clear reason for its resurrection: fatigue with giant language ecosystems.
Think about a developer's daily life today. Start a Node.js project and hundreds of dependencies land in node_modules — and in June 2026 we learned that an npm supply chain attack had penetrated all the way into Red Hat Cloud Services. Python environments are a maze of virtualenvs and packaging tools. Rust is powerful, but compile times and the learning curve are far from trivial. Now that AI coding agents write the boilerplate for us, there is, paradoxically, a growing thirst for "a small tool I can understand in its entirety."
Small languages are one answer to that thirst. A world where you can read the entire language specification over a weekend, where the runtime is a single binary, where the dependency graph fits in your head. In this article we explore what small languages can offer, centered on Lua, the standard-bearer of embeddable scripting, and Janet, the rising star.
What Is an Embeddable Language
An embeddable language is one designed primarily to run inside a host application rather than as a standalone program. Its key characteristics are:
- The runtime ships as a C library that links into the host program
- There is a clear API (binding interface) for exchanging values between host and script
- The runtime is small (on the order of hundreds of kilobytes) with fast startup
- The host can control memory usage and execution (sandboxing, resource limits)
The typical structure looks like this:
+--------------------------------------------------+
| Host application (C/C++/Rust) |
| |
| +--------------------+ +-------------------+ |
| | Core engine | | Script VM | |
| | (perf critical) |<->| (Lua / Janet) | |
| | rendering, IO, | | game logic, | |
| | physics | | config, plugins | |
| +--------------------+ +-------------------+ |
| C API boundary: stack/registry value exchange |
+--------------------------------------------------+
The advantage of this structure is clear. Performance-critical parts are written in the host language; the parts that change often and need flexibility are written in the script. The game industry has used this pattern for decades to get both development speed and runtime performance.
Lua — The Textbook Embeddable Language
Lua was born in 1993 at PUC-Rio in Brazil and has reigned for more than thirty years as the standard for embeddable languages. Its list of success stories is a cross-section of the software industry:
- Games: World of Warcraft addons, Roblox (with its derivative Luau), Garry's Mod, Defold, and the scripting layers of countless game engines
- Web infrastructure: OpenResty, built on nginx, lets you write request handling logic in Lua, and Cloudflare famously ran its early edge logic on this stack
- Databases: the Redis EVAL command executes atomic operations via Lua scripts
- Editors: Neovim adopted Lua as its configuration and plugin language, effectively replacing VimScript
The reason Lua is used so widely lies in its design philosophy. The entire interpreter is written in roughly thirty thousand lines of ANSI C, has no external dependencies, and compiles into a library of a few hundred kilobytes. The language itself is small too. There is essentially one data structure, the table, and it expresses arrays, hash maps, objects, and modules alike.
A Taste of Lua Syntax
-- Variables and tables: the only composite data structure in Lua
local config = {
name = "my-server",
port = 8080,
tags = { "web", "production" },
}
-- Functions are first-class citizens
local function greet(name)
return "Hello, " .. name .. "!"
end
print(greet(config.name))
-- Iterating over a table
for i, tag in ipairs(config.tags) do
print(i, tag)
end
-- Emulating OOP with metatables
local Animal = {}
Animal.__index = Animal
function Animal.new(name, sound)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound
return self
end
function Animal:speak()
print(self.name .. " says " .. self.sound)
end
local dog = Animal.new("Rex", "woof")
dog:speak() -- Rex says woof
Simple syntax means easy learning, but more importantly it means a small, fast implementation. And with LuaJIT, an outstanding JIT compiler, certain workloads run at speeds approaching C.
Embedding Lua from C
The Lua C API is stack based. Host and VM exchange values through a virtual stack.
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
/* Expose a C function to Lua: add two numbers */
static int l_add(lua_State *L) {
double a = luaL_checknumber(L, 1);
double b = luaL_checknumber(L, 2);
lua_pushnumber(L, a + b);
return 1; /* number of return values */
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
/* Register the C function */
lua_register(L, "add", l_add);
/* Run Lua code */
if (luaL_dostring(L, "print('from lua:', add(3, 4))") != LUA_OK) {
fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
}
/* Read a Lua global */
luaL_dostring(L, "answer = 42");
lua_getglobal(L, "answer");
printf("answer from lua = %lld\n", (long long)lua_tointeger(L, -1));
lua_close(L);
return 0;
}
Compiling takes one line:
cc host.c -o host -llua -lm
In under fifty lines of code, host and script converse in both directions. This low barrier to entry is the secret behind thirty years of Lua survival.
Janet — Rediscovering the Small Lisp
Janet is a Lisp-family language created by Calvin Rose (also the original author of the Fennel language). Ian Henry's "Why Janet" and his free ebook "Janet for Mortals" are frequently recommended entry points. Janet's selling points can be summarized as follows:
- Single binary: the interpreter, compiler, and REPL fit into one executable of around one megabyte
- C amalgamation: the entire runtime ships as just two files, janet.c and janet.h, so embedding starts by copying them into your project
- Built-in PEG: instead of regular expressions, a PEG (Parsing Expression Grammar) module lives in the standard library, letting you write parsers declaratively
- Event loop and fibers: lightweight concurrency is supported at the language level
- Native executables: the jpm build tool can bundle scripts into static binaries
A Taste of Janet Syntax
# Janet is a Lisp, but bracket/brace literals make it readable
(def config
{:name "my-server"
:port 8080
:tags ["web" "production"]})
(defn greet [name]
(string "Hello, " name "!"))
(print (greet (config :name)))
# Sequence processing: functional style
(def doubled (map (fn [x] (* 2 x)) [1 2 3 4 5]))
(pp doubled) # @[2 4 6 8 10]
# Both mutable and immutable data structures are built in
(def immutable-tuple [1 2 3])
(def mutable-array @[1 2 3])
(array/push mutable-array 4)
# Building a generator with fibers
(def counter
(coro
(for i 0 3
(yield i))))
(each n counter (print n)) # 0 1 2
Janet's Secret Weapon — PEG
The moment regular expressions hit their limits (nested structures, recursive patterns) is the moment PEG shines. A simple key=value config file parser written with PEG looks like this:
(def config-grammar
~{:ws (any (set " \t"))
:key (capture (some (range "az" "AZ" "09" "__")))
:value (capture (some (if-not "\n" 1)))
:line (* :ws :key :ws "=" :ws :value)
:main (some (* (+ :line :ws) (+ "\n" -1)))})
(def result (peg/match config-grammar "host = localhost\nport = 8080\n"))
(pp result)
# => @["host" "localhost" "port" "8080"]
Because the grammar itself is a data structure (a table), composition and reuse come naturally. If you have ever suffered while gluing regex strings together, you will immediately feel the value of this declarative style.
Embedding Janet from C
Janet embedding is often described as even simpler than Lua. Grab the amalgamation files and compile them together — that is all.
#include "janet.h"
#include <stdio.h>
/* Expose a C function to Janet */
static Janet cfun_add(int32_t argc, Janet *argv) {
janet_fixarity(argc, 2);
double a = janet_getnumber(argv, 0);
double b = janet_getnumber(argv, 1);
return janet_wrap_number(a + b);
}
static const JanetReg cfuns[] = {
{"native-add", cfun_add, "(native-add a b)\n\nAdd two numbers."},
{NULL, NULL, NULL}
};
int main(void) {
janet_init();
JanetTable *env = janet_core_env(NULL);
janet_cfuns(env, "host", cfuns);
Janet out;
janet_dostring(env,
"(print \"from janet: \" (native-add 3 4))",
"main", &out);
janet_deinit();
return 0;
}
cc host.c janet.c -o host -lm -lpthread
Copy two files, invoke the compiler once, and embedding is done. No fights with build systems — that is the practical value of a small language.
What Small Languages Give You
The case for small languages is not sentimental; it is engineering pragmatism.
Total Comprehensibility
You can read the Lua reference manual cover to cover in half a day. The Janet documentation is the same. Being able to hold the entire behavior of the language in your head means that, when debugging, you can rule out "maybe the language did something weird" and focus on your own code. In a giant language, compiler special cases, subtle standard library behaviors, and build tool magic all become suspects.
Fast Startup and Short Feedback Loops
Launching the REPL takes tens of milliseconds, and script execution is instantaneous. There is no losing your focus while waiting for a giant framework to cold-start.
Few Dependencies, Small Attack Surface
As the npm supply chain attacks of 2026 demonstrated, every dependency is attack surface. A small language that solves most problems with its standard library has structurally less supply chain risk.
Small Languages as Configuration — Escaping YAML Hell
Another use for small languages is configuration. YAML is notorious for indentation traps, implicit type coercion (the boolean interpretation of the no keyword, known as the Norway problem), and its lack of abstraction for repetition. Anyone who has maintained hundreds of lines of Helm values files or CI config via copy-paste will sympathize.
Use a scripting language for configuration and you get variables, functions, and conditionals for free.
-- config.lua: programmable configuration
local base = {
image = "myapp",
replicas = 2,
}
local envs = {}
for _, name in ipairs({ "dev", "staging", "prod" }) do
local cfg = {}
for k, v in pairs(base) do cfg[k] = v end
cfg.namespace = "myapp-" .. name
if name == "prod" then cfg.replicas = 6 end
envs[name] = cfg
end
return envs
Repetition and branching are language features, so no YAML acrobatics with anchors and merge keys are needed. This is exactly why the Neovim community migrated en masse from init.vim to init.lua. When configuration becomes code, validation, modularization, and reuse become natural.
A Primer on DSL Design — An Even Smaller Language on Top of a Small One
Embeddable languages are also excellent foundations for DSLs (domain-specific languages). There are two main approaches:
- Internal DSL: design domain vocabulary within the syntax of the host scripting language. Janet, being a Lisp, is particularly strong here thanks to macros.
- External DSL: define your own syntax and write a parser. Janet's built-in PEG dramatically lowers this barrier.
Here is a mini internal DSL built with Janet macros: a declaratively defined HTTP routing table.
(defmacro defroutes [name & routes]
~(def ,name
,(map (fn [[method path handler]]
{:method method :path path :handler handler})
routes)))
(defroutes app-routes
[:get "/users" list-users]
[:post "/users" create-user]
[:get "/health" health-check])
# Macro expansion result: an array of plain data structures
# The router implementation just iterates over this array
Since macros transform code into data at compile time, you gain domain vocabulary at zero runtime cost. Building an interface that "reads like configuration but is actually code" is the essence of internal DSLs.
The Other Candidates — A Map of the Embeddable Language Ecosystem
Beyond Lua and Janet, several small languages are worth a look:
- Wren: a class-based scripting language by Bob Nystrom, author of "Game Programming Patterns" and "Crafting Interpreters". Familiar syntax, fiber-based concurrency, and an implementation famous for being a pleasure to read
- Gravity: created by Marco Bambini for the Creo development environment, with syntax reminiscent of Swift and a small runtime written in C
- Fennel: not a new runtime but a Lisp that compiles to Lua. You keep the entire Lua ecosystem (LuaJIT, Neovim, game engines) while gaining Lisp syntax and macros
- Rhai: an embedded scripting language for the Rust ecosystem. Integration with Rust types is seamless, and its defaults are sandbox-oriented (operation count limits and so on), making it a good fit for safe user scripting
- mruby: a lightweight embeddable implementation of Ruby, an option for teams that prefer Ruby syntax
- Squirrel: a language long used in the game industry, with a history of adoption in some Valve titles
A Selection Criteria Table
A comparison table to consult when choosing which language to embed:
| Criterion | Lua | Janet | Wren | Rhai | Fennel |
|---|---|---|---|---|---|
| Syntax family | Procedural | Lisp | Class-based | Rust-like | Lisp |
| Runtime size | Very small | Small | Very small | Small | Depends on Lua |
| Host language | C | C | C | Rust | Lua runtime |
| JIT option | LuaJIT | None | None | None | LuaJIT |
| Ecosystem size | Very large | Small but active | Small | Growing in Rust | Shares Lua ecosystem |
| Concurrency | Coroutines | Fibers, event loop | Fibers | Delegated to host | Coroutines |
| Parsing tools | External libraries | Built-in PEG | External | External | Lua assets |
| Proven domains | Games, infra | CLI, scripting | Games, learning | Rust app extension | Neovim, games |
A practical summary guide:
- If a proven ecosystem and performance (LuaJIT) come first, pick Lua
- If single-file embedding, PEG, and Lisp macros appeal to you, pick Janet
- If your host is Rust, Rhai has the lowest integration cost
- If you already use a Lua-based host (such as Neovim), upgrade just the syntax with Fennel
Production Considerations
When putting a small language into a user-facing product, two things deserve deep scrutiny.
Sandboxing
If you execute scripts written by users, isolation is mandatory. The checklist:
Sandboxing checklist
[ ] Block dangerous modules: have you removed file IO, process
execution, and network access from the environment
(for Lua, remove the os and io tables)
[ ] Defend against CPU runaway: do you have a way to break
infinite loops (instruction count via Lua debug hooks,
operation limits in Rhai, separate thread + timeout, etc.)
[ ] Memory limits: do custom allocators or GC caps prevent
memory gluttony
[ ] Stack depth limits: can a recursion bomb kill the host
[ ] Error boundaries: do script errors stay contained instead
of crashing the host (run via protected calls)
For Lua, sandboxing by restricting the environment table is established practice; Rhai was designed with restricted execution in mind from the start. Either way, "run user code in the default environment as-is" is forbidden.
Performance
You must understand the cost of crossing the interpreter call boundary.
- Reducing the number of host-script boundary crossings is the number one optimization. Call into the script per pixel and any language will be slow. Make coarse calls per frame or per event
- If Lua is the bottleneck, consider switching to LuaJIT. But evaluate that LuaJIT targets Lua 5.1 compatibility and has a maintainer structure separate from upstream
- Janet has no JIT, so the canonical design pushes numeric loops down into C functions
- If GC pauses are a problem, tune incremental GC settings (Lua) or control when manual GC runs
Pitfalls and Counterarguments — Balancing with Big Language Ecosystems
The praise of small languages must be weighed against real counterarguments.
First, the absence of an ecosystem is a real cost. Build a web service in Janet and you will write authentication libraries, ORMs, and cloud SDKs yourself or patch the gaps with C bindings. In territory the standard library does not reach, the price of "fully comprehensible" is "fully self-implemented."
Second, hiring and onboarding. The probability that your team includes a Janet veteran is low. The language is small so learning is fast, but the shallow accumulation of idioms and best practices affects code review quality.
Third, compatibility with AI tooling is a practical variable. As of 2026, coding agents are strongest in Python and TypeScript, where training data is abundant. In minor languages hallucinations are frequent, so AI-assisted productivity — the new advantage of big languages — becomes a relative weakness of small ones. That said, a small language permits a counter-move: you can hand the agent the entire language specification as context. Context engineering tricks like keeping a Janet cheat sheet in CLAUDE.md actually work.
The conclusion is not a dichotomy. A hybrid — the product core in a mainstream language, with extension points, configuration, and domain logic in a small embedded language — is the equilibrium that thirty years of Lua history has proven.
A Hands-On Guide — Start This Weekend
A step-by-step path to experiencing small languages:
- Install Janet and play in the REPL for thirty minutes. The official tutorial is enough
# macOS
brew install janet
janet -e '(print "hello, small world")'
# Building from source is simple too
git clone https://github.com/janet-lang/janet.git
cd janet && make && sudo make install
- Rewrite one everyday script in Janet or Lua. If it involves log parsing, you will feel the true power of Janet PEG
- A mini C embedding project: based on the example code above, drill one script hook into a program of your own
- Migrate one configuration file to Lua: repetitive YAML is a good candidate
- If you want to go deep, read "Crafting Interpreters" and build a small language yourself. Written by the author of Wren, it teaches embeddable design sensibilities firsthand
Closing Thoughts
The renewed buzz around "Why Janet" is not mere nostalgia. Amid dependency fatigue, supply chain anxiety, and the accumulation of tooling complexity, it is a signal that the value of small, fully comprehensible tools is being reappraised.
Small languages do not replace big ones. Instead they act as shock absorbers in the crevices of large systems — configuration, extension, DSLs, user scripting — soaking up complexity. Lua proved it in games and infrastructure, and Janet carries that lineage forward with more modern tooling. Invest one weekend day and fire up the REPL. The feeling of understanding an entire language is a greater pleasure than you might expect.
References
- Why Janet (Ian Henry): https://ianthehenry.com/posts/why-janet/
- Janet official site: https://janet-lang.org/
- Janet for Mortals (free ebook): https://janet.guide/
- Lua official site: https://www.lua.org/
- Lua reference manual: https://www.lua.org/manual/5.4/
- Programming in Lua: https://www.lua.org/pil/
- LuaJIT: https://luajit.org/
- Fennel: https://fennel-lang.org/
- Wren: https://wren.io/
- Rhai: https://rhai.rs/
- Crafting Interpreters: https://craftinginterpreters.com/
- Hacker News: https://news.ycombinator.com/
- GeekNews: https://news.hada.io/