# Berry: a scripting language sized for microcontrollers

> Berry is a dynamically typed embedded scripting language whose interpreter core fits in under 40KiB of code and runs on less than 4KiB of heap, with a one-pass compiler and a register-based VM written in ANSI C99. It suits firmware authors who need configurable behaviour on a microcontroller, and it partly borrows its design from Lua.

**berry-lang/berry** — A ultra-lightweight embedded scripting language optimized for microcontrollers.

- Repository: https://github.com/berry-lang/berry
- Website: https://berry-lang.github.io
- Stars: 1,072 · Forks: 138
- Language: C
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/berry-lang-berry

## A footprint budget, stated in kilobytes

Berry is a dynamically typed embedded scripting language designed for lower-performance embedded devices, and the numbers in the documentation are specific enough to plan against. The interpreter core's code size is less than 40KiB and it can run on less than 4KiB of heap, measured on an ARM Cortex M4 CPU with the Thumb ISA and the ARMCC compiler. That measurement caveat is important. A Cortex M4 with ARMCC is a small target, and the number does not transfer automatically to a different architecture or a different toolchain, so anyone budgeting for a specific board should measure rather than assume. The primary language is C, and the whole interpreter, including the compiler and the virtual machine, is written in ANSI C99. The licence is MIT, with the note that the interpreter partly refers to Lua's design and points at Lua's own licence.

## A one-pass compiler feeding a register-based VM

The architecture is two stages. The interpreter bundles a one-pass compiler that emits bytecode, and a register-based virtual machine that executes it. A one-pass compiler means the source is compiled as it is read rather than in a separate parse-then-emit pass, which is what keeps the implementation small enough for the target. A register-based VM means the bytecode works on a fixed set of registers rather than on an operand stack, so arithmetic does not repeatedly push and pop values. The documentation notes that in Berry not every type is a class object. Simple value types such as int, real, boolean and string are not class objects, while list, map and range are class objects, and the project calls this out explicitly as a performance consideration. That distinction is the sort of thing that decides how you write idiomatic Berry code, since operations on a plain int skip the object protocol entirely.

## Values that are not objects, containers that are

The type system is small and worth reading as a list. The base types are nil, the booleans true and false, the numerical types int and real, strings in single or double quotation marks, class as a read-only instance template, instance as an object constructed from a class, module as a read-write key-value pair table, list as an ordered container such as `[1, 2, 3]`, map as a hash map container such as `{ 'a': 1, 2: 3, 'map': {} }`, and range, which includes a lower and an upper integer value such as `0..5`. Ranges being first-class class objects is unusual enough to be worth trying, and the presence of a dedicated range type hints at the slicing and iteration patterns the language is tuned for. The operator set is complete rather than exotic: assignment including the compound forms, relational and logic operators, arithmetic, bitwise, field access, subscripting, string concatenation, the conditional operator, and parentheses.

## Closures, single inheritance and destructors

Berry supports imperative, object-oriented and functional styles at once, and the functional part is real. Functions have local variables with block scope, a return statement, nested function definitions, closures based on upvalues, anonymous functions and lambda expressions. The object-oriented side is deliberately narrow: inheritance is limited to public single inheritance, and a class can define methods, operator overloads, a constructor method and a destructive method. Single public inheritance with operator overloading is a small, predictable surface, which suits a language whose interpreter has to be small. The combination means a firmware author can write a callback-heavy DSL, dispatch on behaviour through classes, and use lambdas for inline handlers, all without leaving a single language.

## Building it with make and the readline dependency

The build is a plain make invocation, and the one dependency is readline, which Windows does not need:

```bash
sudo apt install libreadline-dev # Ubuntu
brew install readline            # MacOS
```

Then build with the default compiler, which is GCC:

```bash
make
```

The Makefile shows the settings the project actually builds with: `-Wall -Wextra -std=c99 -pedantic-errors -O2`, linking against `-lm`, targeting a binary called `berry`, with `PREFIX = /usr/local`. On non-Windows platforms the flags add `-DUSE_READLINE_LIB` and link `-lreadline -ldl`, and on Linux the linker gets `-Wl,--export-dynamic`, which is what lets the runtime resolve native symbols for the FFI. There is also a `debug` target that adds `-O0 -g -DBE_DEBUG` and a `test` target that enables coverage with `-fsanitize=address -fsanitize=undefined` before running the test suite through `./testall.be`. The project also ships a CMakeLists.txt, and on Windows the build needs Python to run the code generator.

To run the interpreter after building:

```bash
./berry # Bash or PowerShell
berry   # Windows CMD
```

On Unix-like systems you can install it with `make install`.

## Running a script file, and what the REPL looks like

Running the `berry` command with no parameters enters the REPL, and the documented banner is a plain version line, the compiler identification and the prompt. To run a file instead, pass the path:

```bash
./berry test.be
```

The examples in the repository are `.be` files, and the set gives a good sense of the language. There is `fib_rec.be` for recursion, `bintree.be` for a binary tree, `qsort.be` and `calcpi.be` for classic algorithms, `json.be` for parsing, `listdir.be` for directory listing, `exception.be` for the error handling model, `lambda.be` and `anon_func.be` for the functional forms, `strmod.be` and `string.be` for string handling, `bigloop.be` for a hot loop, `repl.be` and `guess_number.be` for the interactive cases, plus `testall.be` at the repository root which is the entry point the make test target runs. Exception handling follows a familiar shape: you throw any exception value with the `raise` statement, and there are multiple catch modes.

## Three ways to load a module, with different trade-offs

The module system is where an embedded language has to make hard choices, and Berry offers three. The built-in module takes almost no RAM, because constant objects are stored in read-only code data segments at compile time, so the interpreter's RAM usage stays low when it starts. Beyond that there are extension modules: a script module, a bytecode file module, and a shared library module, described as being like `*.so` or `*.dll`. The three are not interchangeable. A script module is source you ship, which costs parsing time on the device. A bytecode file module is precompiled bytecode, which trades a build step for startup speed. A shared library module is native code, which is where the FFI lives and where you can call into the host system. Bytecode support is also a feature in its own right: you can export a function to a bytecode file and load and execute it, which is a way to ship logic to a device that already has the interpreter.

## Where Berry is the wrong choice

The obvious limits follow from the design. Berry is dynamically typed, so there is no compile-time type checking on your script, and a typo in a variable name is a runtime failure on a device that may be hard to reach. Garbage collection is mark-sweep, so a device with a small heap and long-lived allocations needs care about what the collector can reclaim. The documentation does not describe a way to disable collection for a hard-real-time path, and it does not document priority inversion or a bounded execution model for scripts that must not block a control loop, so if your firmware has a deadline you have to enforce it yourself around the interpreter. The environment is also thin in a specific way: there is no package manager, no test framework beyond the project's own `testall.be` suite, and the release history is short, with v1.1.0 dated 2022-08-01 and v0.1.0 dated 2019-06-22. The project is not archived and the last push was on 2026-10-10, so work continues even though the tagged releases are old.

## Conclusion

Adopt Berry when you are writing firmware for a microcontroller and need behaviour that can be changed without reflashing, and you can live with a dynamically typed language and a mark-sweep collector. Skip it if your device has comfortable memory and you need static typing, since Berry trades compile-time guarantees for a small footprint, and do not expect the reference manual to cover embedding specifics the way a host SDK document would. Before committing, measure the interpreter core against your flash and RAM budget, since the stated figures of under 40KiB of code and less than 4KiB of heap are for an ARM Cortex M4 with the ARMCC compiler, and read the extension module rules before designing a native binding, because script, bytecode file and shared library modules each load differently.

## FAQ

### How small is the Berry interpreter on a microcontroller?

The interpreter core's code size is less than 40KiB and it can run on less than 4KiB of heap, measured on an ARM Cortex M4 CPU with the Thumb ISA and the ARMCC compiler.

### How do I build Berry from source?

Install readline (not needed on Windows), then run `make` with the default GCC compiler. The Makefile compiles with -Wall -Wextra -std=c99 -pedantic-errors -O2 and produces a binary named berry. On Unix-like systems `make install` installs it under PREFIX /usr/local.

### Which types in Berry are class objects?

List, map and range are class objects, while the simple value types int, real, boolean and string are not. The project calls this out as a performance consideration.

### What kinds of extension modules does Berry support?

Extension modules can be script modules, bytecode file modules, and shared library modules such as *.so or *.dll. The built-in module takes almost no RAM because constant objects are stored in read-only code data segments.

### How do I run a Berry script file instead of the REPL?

Pass the file path to the berry command, for example `./berry test.be`. The repository's examples directory contains .be files such as fib_rec.be, qsort.be and json.be.

## Sources

- [berry-lang/berry on GitHub](https://github.com/berry-lang/berry)
- [License: MIT](https://github.com/berry-lang/berry/blob/master/LICENSE)
- [Project website](https://berry-lang.github.io)
- [README](https://github.com/berry-lang/berry/blob/master/README.md)
- [Releases](https://github.com/berry-lang/berry/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/berry-lang-berry
