Changelog
All notable changes to Bee are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.3.4] - 2026-08-10
Added
- Ahead-of-time compiler (
beec). A program can now be compiled to a standalone native executable that runs without the interpreter:
beec reuses the lexer, parser, and resolver, then generates C++ in which
each Bee function becomes a native function and variables become
reference-counted cells (so closures capture correctly); every operation is
delegated to the same runtime the interpreter uses, linked in as a static
library (libbee_runtime.a). The whole program's control flow and calls are
machine code - no bytecode or AST is interpreted at run time.
The compiler covers the whole language: variables and all operators,
strings/lists/dicts, indexing and slicing, if/while/for/for..in,
break/continue, match, functions, closures, recursion, first-class
functions, default and ...rest parameters, classes with inheritance
(this/super/new, custom str()), destructuring let, list
comprehensions, try/catch/finally/throw, threads, and import
(resolved as the interpreter does -- sibling .bee/.be files, a lib/
folder, hive_modules/ packages up the tree honouring each hive.json
main, $BEE_PATH, and ~/.hive/lib -- and compiled into the same binary,
dependencies and all). Only importing a native C/C++ .so extension is
unsupported, and it is reported with a file and line rather than
miscompiled. See docs/COMPILING.md. AOT output is
standalone but not yet type-specialised, so numeric hot loops still run
fastest under the JIT.
0.3.3 - 2026-08-10
Fixed
- Windows release build. The JIT backend DLL failed to link on MSYS2 with
export ordinal too large: MinGW auto-exports every global symbol, and with LLVM folded in statically that far exceeds the 65535-entry PE export table. The DLL now links with--exclude-all-symbolsand exports only its one entry point (bee_jit_create, via__declspec(dllexport)), which is allbee.exeresolves. This is what kept the 0.3.2 release job red.
0.3.2 - 2026-08-10
Language
- Optional type annotations. Parameters, variables and return types can be declared, and are checked where the value enters:
The type names are num, str, bool, list, dict, buffer, nil,
fn, any, and any class name; a class annotation accepts instances of that
class and of anything deriving from it. A violated annotation is a runtime
error naming the parameter, the declared type and what arrived, reported at
the call site with the usual stack trace. Return types are checked on every
path out, including falling off the end - a -> num function that returns
nothing yields nil, which is an error.
Types are entirely optional and mix freely with untyped code: an unannotated
parameter is any, a typed function can call an untyped one, and a program
with no annotations behaves exactly as before and runs at exactly the same
speed (the per-call check is skipped by a single flag test on the
declaration). All four ways into a function - the tree-walker, the bytecode
VM, a direct VM-to-VM call and a fused method call - enforce them identically,
which the differential suite checks.
An annotation binds the name for its whole life, not just its initialiser:
assigning a string to something declared num is an error wherever it
happens, and a parameter's default value has to satisfy the annotation too.
That is what makes the declaration something later code can rely on.
- Declared types generate better code. The compiler tracks what each
register is known to hold and, where both operands are declared numbers, emits
arithmetic with no type test and no fallback branch. A declared
bufferindexed by a declarednumcompiles to a bounds check and a direct load against contiguous unboxed memory, with no call into the interpreter.
A dot() kernel over two buffers went from 0.115 s to 0.089 s by adding
annotations. A plain arithmetic loop gained almost nothing (0.715 s → 0.691 s),
which is the more informative result: the type test was never the cost, and
the remaining work is to stop building a Value for numbers at all - see
docs/PERFORMANCE.md.
-
BEE_DUMP_BYTECODE=1prints each function's bytecode as it is compiled. -
The JIT compiles per argument signature, and buffers reach native code. It previously accepted unboxed numbers and nothing else, so any function touching a container was never compiled. Each call now classifies what it is actually passing - number, f64 buffer, or something else - and that signature keys the compilation. A call whose arguments do not match a compiled signature finds none and runs interpreted, so the argument types are themselves the guard: there is nothing to invalidate and no state to reconstruct.
An f64 buffer is passed as a raw base pointer and an element count, so b[i]
compiles to a bounds check and a load against contiguous unboxed memory. A
dot() kernel over two buffers went from 0.089 s to 0.026 s - 3.3× faster
than CPython, and the gap to Go closed from 22.3× to 6.5×.
This needs no annotations: the signature comes from what the call passes, not
what the source declares, so an untyped fn dot(a, b, n) compiles just as
well. Buffer reads are compiled and writes are not, which preserves the
contract that makes bailing out safe - native code that gives up is re-run by
the interpreter, and re-running can only be correct if nothing was written.
Measured in isolation, a compiled dot() runs at 1.01 ns per element
against 44.79 ns on the bytecode VM. With process startup excluded, that is
3.67× off Go and 6.7× faster than CPython.
-
The LLVM engine is built on first use, not at startup. Standing up an ORC session cost every run of
beea couple of milliseconds for a JIT most scripts never trigger. -
BEE_NO_JIT=1runs everything on the bytecode VM, the counterpart ofBEE_NO_VM=1. Both exist so a measurement can be attributed to a tier rather than guessed at. -
bench/run.shreports aworkcolumn with each runtime's startup floor subtracted. Bee starts in ~15 ms against CPython's ~12 ms and a Go binary's ~1 ms, so on a benchmark doing 10 ms of work, comparing totals said more about process startup than about either language.
Performance
- The LLVM JIT is a dlopen'd shared object, not a link-time dependency. The
backend used to be linked straight into
bee, so the dynamic loader mapped ~120MB oflibLLVM.soon every invocation - measured here at ~6ms of pure startup (an interpreter-only build starts in <1ms), paid whether or not a script ever compiled anything. Most scripts never trigger the JIT, so this was the single broadest cost a person felt running one.
The backend now lives in libbee_jit.so, which bee dlopens the first time
a function or loop is hot enough to compile; the executable itself links no
LLVM at all. A plain run maps the library never, and starts at the
interpreter-only floor. When compilation is triggered the library loads
once, transparently - fib(34) still runs ~10× faster than the VM. The shared
object calls back into the interpreter through the same -rdynamic symbol
resolution native modules use, and this is the same technique beegen already
uses for libclang. If the library is missing or fails to load, execution
simply falls back to the interpreter/VM. Set BEE_JIT_LIB to point at a
specific backend.
The Windows installer now ships the JIT too - as bee_jit.dll, built under
MSYS2 UCRT64 against an ABI-matched LLVM and dlopen'd by bee.exe the same
way. Previously the Windows build was interpreter-only. The DLL is
self-contained and shares bee.exe's heap through the Universal CRT.
-
BEE_NO_JIT=1now also suppresses the loop JIT, not just the function JIT, so it disables the native tier completely - the counterpart ofBEE_NO_VM=1, and what makes a measurement attributable to one tier. -
A register bytecode VM. Function bodies are compiled once to a flat instruction stream and executed there instead of being walked as a tree. The design is register-based rather than stack-based, so
s = s + xs[i]is two instructions where a stack VM needs six, and dispatch is by computed goto, so each opcode gets its own indirect branch and its own prediction history.
Registers are the resolver's frame slots. That works because a function whose body creates no closure has all of its scopes merged into one frame (see scope merging below), so every local sits at depth 0 with nothing to walk. A frame is a window into one flat, thread-local register array - entering a function is an index, not an allocation.
Anything the compiler does not cover yet - a nested function or class, try,
import, destructuring - makes it decline that one function, which then
runs on the tree-walker exactly as before. Nothing about the language changes,
and BEE_NO_VM=1 runs everything on the tree-walker.
A new suite, tests/vm_diff_test.sh, runs 29 programs on both engines and
requires identical stdout, stderr and exit code - including error messages,
stack traces and uncaught-error exit codes.
-
Scopes that nothing can capture are merged into the enclosing frame. A block or loop that declares names used to allocate an
Environmenton every entry - meaning once per iteration for a loop body. When no function or class is created inside it, nothing can outlive the scope, so its variables now live in slots of the enclosing frame and no environment is allocated at all. Where a closure is created, the scope is untouched, so each iteration still captures its own bindings. -
Call frames are recycled. A call allocated an environment (a control block and a slots vector) and dropped it on return. Frames now come from a small free list, and only the ones a closure captured are left to live on.
-
Compiled functions call each other directly. A
CALLwhose target is another compiled Bee function taking exactly those arguments now enters it from the dispatch loop: no argument vector, no trip throughcallFunction, and the callee's registers are filled straight from the caller's. Built-ins, classes, defaults, rest parameters, uncompiled bodies and the numeric functions the LLVM JIT claimed still go through the interpreter, so behaviour -
traces, arity errors, the depth limit - is unchanged. A call-heavy loop dropped from 0.73 s to 0.48 s.
-
Instance fields are shapes, not a map. An instance's fields lived in a
std::map<std::string, Value>, sop.xwas a tree walk with string comparisons - measured at ~41 ns, which was most of the cost of object-oriented code. AShapenow records which field names an instance has and which slot each one occupies; instances that gained their fields in the same order (for a class with a normalinit, all of them) share a single shape, so the fields themselves are a flat vector. Each property site caches "this shape means slot N" and validates it with a pointer compare.
Field access is 4.0× faster and now beats CPython. One class reaching two different shapes, fields added after construction, and a field shadowing a method of the same name all still behave as before.
-
obj.m(args)is one instruction. It used to be a property read followed by a call, and readingobj.mon its own has to produce a callable - which for a method meant allocating a copy of it bound to the receiver, on every call. The receiver is now passed directly, with the method resolved once per class at each call site. Instance method dispatch is 28.7× faster than 0.3.1. -
A benchmark suite, in
bench/: 17 programs covering loops, calls, methods, classes, dicts, strings, closures, sorting and error handling, with equivalents in Python and Go.make benchtimes the current build;bench/run.sh --vs <other-bee> --allcompares against another build, CPython and Go at once. Contenders run interleaved and keep their best time, because on a laptop the CPU's boost state drifts enough during a suite to swing back-to-back results by 2×. -
Cheaper calls and indexing: a call frame stores the function pointer and formats its name only when a stack trace is printed;
callFunctiontakes its function by reference rather than byshared_ptrcopy; list and dict indexing borrow the container instead of copying ashared_ptr; and the VM indexes a list by number without leaving the dispatch loop.
Measured against 0.3.1, best of five interleaved runs, 9 M inner iterations:
| 0.3.1 | now | |
|---|---|---|
nested loop over a list, s = s + xs[i] |
1.13 s | 0.21 s (5.4×) |
the same with if (xs[i] % 2 == 0) |
1.47 s | 0.38 s (3.9×) |
the same written with continue |
20.57 s | 0.38 s (54×) |
xs[i] = xs[i] + 1 |
0.45 s | 0.09 s (5.0×) |
a let in the loop body |
1.08 s | 0.17 s (6.4×) |
| loop calling an interpreted function | 4.98 s | 0.44 s (11.3×) |
Against CPython 3.12 on the same three list benchmarks, Bee went from 3.9×, 3.7× and 52× slower to 0.80×, 1.07× and 1.05× - at or slightly ahead of CPython, where the whole interpreted path used to sit well behind it. Geometric mean across the whole suite: 3.94× faster than 0.3.1, with object-oriented code 17-29× faster.
Changed
- The language is now called "Bee", not "BeeLang". Prose across the README,
docs and examples uses the shorter name; the binary (
bee), the package manager (hive), file extensions (.be/.bee), the GitHub organisation and every URL and install path are unchanged, so nothing about an existing setup breaks.
Fixed
-
String building was quadratic inside a compiled function.
s = s + xhas grown its buffer in place since 0.1.1, but the bytecode VM did not carry that over, so building a 3 MB string took 363 s instead of 0.06 s. Every correctness test passed throughout - the answer was right, only the complexity was wrong - sotests/perf_guard_test.shnow checks that this and four other idioms stay linear. -
sort(xs, cmp)[0]could read freed memory. Indexing straight off a call result compiles to an instruction whose destination register is also its object register; writing the element back into that register dropped the last reference to the list and freed it mid-read. It produced a denormal double for small inputs and a crash for large ones. Both this and the aliasing shape behind it are covered by the differential suite now. -
break,continueandreturnno longer throw C++ exceptions. They werethrow BreakSignal{}/ContinueSignal/ReturnSignal, caught by every loop and by every call - which cost 2.4-3.8 µs each, 50-100× an entire interpreted loop iteration, on paths that are not exceptional at all: every function call returns, and acontinueruns on most iterations of the loop that uses it. A statement now reports how it finished (Flow::Normal,Break,Continue,Return) and the enclosing loop or call frame absorbs it.
Measured against the previous release, best of three runs:
| before | after | |
|---|---|---|
nested loop with continue in the body (9 M iterations) |
20.33 s | 1.39 s |
| loop calling an interpreted function (1 M calls) | 2.45 s | 0.23 s |
continue microbenchmark (1 M) |
3.85 s | 0.05 s |
nested loop summing xs[i] (9 M) |
2.18 s | 1.04 s |
The last row is a side effect worth having: dropping the try region from
around each loop-body execution let the C++ compiler optimise a loop it
previously could not touch.
Nothing about the language changes. finally still runs on every exit path,
and a return out of a finally still wins over one out of the body. A Bee
throw, and runtime errors, remain C++ exceptions - those really are rare.
-
Cheaper calls and indexing. A call frame stores the function pointer and formats its name only when a stack trace is printed, instead of building and copying a
std::stringon every call;callFunctiontakes its function by reference rather than byshared_ptrcopy; and list/dict indexing borrows the container instead of copying ashared_ptr, removing two atomic refcount operations perxs[i]. -
docs/PERFORMANCE.md- a measured account of where the interpreter spends its time, with per-operation costs, comparisons against CPython, and the remaining plan (stack frames instead of heap environments, a bytecode VM, a cheaper value representation, and a type-guarded JIT that deoptimises instead of refusing to compile).
0.3.1 - 2026-08-07
Packaging. Installing a package that contains a native module no longer means
compiling one by hand, packages are a binary format rather than a text file, and
a bug that made 0.3.0's .deb unable to build any native module is fixed.
Fixed
- 0.3.0's
.debshipped an unusable header set.packaging/build-deb.shinstalledsrc/*.hpp, butbee_buffer.his a plain C header - andbee_native.hppincludes it. So every native module compiled against an installed Bee failed withfatal error: bee_buffer.h: No such file or directory. Both extensions are installed now, and the packaging script fails rather than shipping a header set that can't compile a module.
Added
- Prebuilt native modules. A package lists binaries per platform, and
hive installuses the matching one instead of compiling:
"binaries": {
"linux-x86_64": "prebuilt/linux-x86_64/net_native.so",
"windows-x86_64": "prebuilt/windows-x86_64/net_native.dll"
}
Installing should be a download, not a build. Compiling is now only what happens on a platform a package doesn't ship for.
- A package can declare how to build itself.
hive.jsongains a"build"command, run once in the package directory after install - the fallback when no prebuilt matches:
The command is printed before it runs, since it came from a downloaded
package; hive install --no-build skips it. A failed build shows its output
and exits non-zero but leaves the files in place, because the cause is usually
a missing compiler and re-running it by hand is then the whole fix.
Note that a .pkg does not carry the executable bit, so a build command needs
to name its interpreter: bash build.sh, not ./build.sh.
Changed
- Packages are now
.pkg, and no longer plain text.hive packwrites<name>-<version>.pkginstead of.hive, in a newBEEPKG1container: the payload is compressed with a small built-in LZSS and then XORed with a keystream. That roughly halves a package and makes it a binary blob rather than a text file with the sources sitting in it.
The obfuscation is worth naming for what it is: it is not encryption. The
key is a constant in src/hive/archive.cpp, and anything that can install a
package can extract one. It stops a package being browsed or hand-edited in a
text editor; it keeps nothing secret. Integrity is unchanged and is the part
that carries weight - per-file SHA-256, rejected paths, and a loud failure on
truncated, padded or tampered bytes. An old HIVE1 archive is reported as
such, with the suggestion to repack it.
This is a breaking format change. No registry is running yet, so nothing
published needs migrating; repack any local archives with hive pack.
0.3.0 - 2026-08-07
The interoperability release. Bee can now call C and C++ libraries without
rebuilding the interpreter: native modules import like any other module,
beegen generates the binding for you from a header, and buffers give
bulk data somewhere to live that isn't a million boxed Values - and cross the
native boundary without a copy. OpenCV 4.6 was bound and driven end to end to
prove the set is sufficient for a real library, not just a toy one. 180 checks,
up from 100.
Added
- Native modules. A shared library can now be imported like any other module, so calling a C or C++ library no longer means rebuilding the interpreter:
extern "C" const char* bee_native_abi() { return BEE_NATIVE_ABI; }
extern "C" int bee_module_init(bee::NativeModule* m) {
m->def("add", 2, [](https://github.com/playatanu/bee/blob/main/bee::Interpreter&, std::vector<bee::Value>& a) {
return bee::Value(bee::native::num(a[0], "add", 0) +
bee::native::num(a[1], "add", 1));
});
return 0;
}
import demo finds demo.so (.dll, .dylib) through the same lookup as a
.bee file, including inside an installed package - so a hive package can ship
a compiled module. src/bee_native.hpp is the API, with
conversion helpers that report a bad argument as a normal Bee error, with a
stack trace.
beegen, a binding generator. It reads C++ headers with libclang and writes a native module plus an idiomatic Bee wrapper:
Free functions, classes (constructors, methods, public fields, statics) and
enums are mapped; C++ classes become Bee classes holding an opaque handle,
enums become dicts. Every declaration it can't map is reported with a
reason - templates, variadics, out-parameters, unbound types - because a
binding that silently omits half a library is worse than one that says so.
It also writes hive.json, so a binding installs like any other package. Full
documentation in docs/BINDINGS.md.
libclang is loaded at run time through a hand-declared slice of its stable C ABI, so building Bee needs no clang headers or libraries at all.
- Buffers: a contiguous typed array. Bee's answer to an ndarray, and the type bulk data travels in:
let img = zeros([480, 640, 3], "u8") # 900 KB contiguous, not 15 MB of Values
print(img) # buffer<u8>[480,640,3] [0, 0, ...]
f32/f64/i8/u8/i16/u16/i32/i64, flat [] indexing plus at /
set_at per dimension, zeros/ones/full/buffer_from/to_list,
shape/dtype/byte_len, reshape/astype/copy/fill, and
buf_add/sub/mul/div/sum/min/max. Buffers compare by contents and
print with a preview rather than a million elements.
-
Zero-copy buffers across the native boundary. A shim declares a parameter as
BeeBuffer- a plain C struct in the newbee_buffer.hthat needs no Bee header - andbeegenhands the buffer's own memory over by pointer. This is what makes binding an image or tensor library practical rather than merely possible. -
Native code can call back into Bee.
bee::native::callback()wraps a Bee function so a library's log or progress hook can invoke it, withGilLockfor callbacks arriving on the library's own threads andGilOfffor handing the lock back during a long call. The interpreter is now linked with-rdynamicso a module can resolveInterpreter::callValue. -
Class hierarchies and factory-made interfaces. A derived handle is accepted where a base is expected, through a registered
static_cast(so the pointer is adjusted correctly even under multiple inheritance). Abstract classes get no constructor and nofree()- their instances come from a factory function and are released by the API's owndestroy(). This is the shape TensorRT and ONNX Runtime expose, and it is now bindable directly. -
std::vector<T>maps to a list in both directions, for numeric, bool and string elements. -
Wrappers adopt factory handles.
new Image(vision.imread("cat.png"))works as well asnew Image(): a generated wrapper either constructs a new object or takes over a handle a factory function returned. Library APIs of any size are full of factories, so a wrapper that could only construct could not be used with them at all. -
Verified against a real library. OpenCV 4.6 was bound through a 40-line shim and driven from Bee end to end -- pixels built in a buffer, handed to OpenCV with no copy, resized, converted, blurred, Canny-detected, written as a PNG, read back and pulled into a buffer again.
-
C++ default arguments become optional Bee arguments:
beegenemits one native entry point per callable arity and the wrapper dispatches on how many arguments it was given, so the C++ compiler supplies the defaults and beegen never has to parse them. -
tests/beegen_test.sh- 53 checks covering generation, the skip report, the wrapper's shape, compiling the generated module, calling it from Bee, the boundary errors (wrong type, wrong arity, wrong handle, use-after-free), and the capability set above against a header shaped like a real inference API.
Changed
-
The
.deband Windows installer also installbeegen, and the.debnow ships Bee's headers under/usr/include/beeso native modules can be compiled against an installed interpreter. -
The VS Code extension moved to its own repository, beelang-project/vscode-bee, with its history intact. It was
editors/vscode-bee/, which tied its releases to the interpreter's - thevscode-beelang-v0.1.0tag matched this repo'sv*release trigger and tried to build a.debout of it. Editor tooling and the language now version and ship independently. The release trigger here is narrowed tov[0-9]*so only interpreter versions fire it.
Fixed
- A C++ parameter named like a Bee keyword (
in,class,fromare all ordinary C++ names) generated code that would not parse. Such names now get a trailing underscore. - A C++ library's own exception no longer aborts the process.
cv::Exception,Ort::Exception,std::bad_allocand friends thrown inside a native call are converted to a Bee runtime error with a stack trace. Bee-levelthrowand control flow still pass through built-ins that call back into Bee code. hive installnow refuses to install a package into its own source tree (which producedgreet/hive_modules/greet, helping nothing) and never records a package as depending on itself.--forceoverrides the first if you really mean it.
Known limitations
beegenneeds libclang at run time - not to build Bee, but to read a header. Without it (apt install libclang-18-dev, or--libclang <path>) it says so and stops. Everything else, including running generated bindings, works without it.- Not every declaration can be bound. Templates, variadics, out-parameters and types beegen can't map are skipped - reported individually, with a reason, rather than silently dropped. Hand-write a shim for those; the OpenCV binding needed 40 lines of one.
- Buffers are dense and contiguous only: no strides, no views, no broadcasting.
reshapeand slicing a buffer copy. - The Windows installer still ships an interpreter-only build (no JIT), and now
no libclang either, so
beegenon Windows needs one installed separately. - Carried from 0.2.0: no public registry is running, so
hive install <name>needs a--registry;hive publishdoesn't exist; the REPL has no line editing or history.
0.2.0 - 2026-08-06
The sharing-and-diagnostics release. Bee gains Hive, a package manager, so code can be published and installed; stack traces, so an error says where it came from instead of just naming a line; and three conveniences the language was missing - string interpolation, slicing, and an interactive REPL. A new test suite covers all of it: 100 checks, plus the 15 examples.
Language
-
String interpolation. An
fprefix substitutes{expressions}into a string -f"{name} has {n * 2} items"- with any expression allowed inside the braces, including strings of its own (f"{d["key"]}"). Write{{and}}for literal braces; a string without the prefix is untouched, so nothing that used to work changes meaning. -
Slicing.
xs[1:3],xs[:2],xs[2:],xs[-2:]andxs[:], on lists and strings. Bounds may be negative (counting from the end) and are clamped rather than checked, soxs[0:100]is the whole list and an inverted range is empty. Slicing a list returns a copy. -
An interactive REPL.
beewith no arguments starts a session: a bare expression prints its value, definitions persist across lines, an open block keeps prompting with...until it closes, and an error doesn't end the session. Two more entry points come with it -bee -e '<code>'for a one-liner andbee < script.bee(or a pipe) to read a program from stdin.
Diagnostics
- Stack traces on every error. An uncaught error now prints the file, line and function for each call on the way in, innermost first:
Runtime error: division by zero
at safe_div() lib/math.bee:8
at total() report.bee:14
at <main> report.bee:31
Methods appear as Class.method(). Built-in failures are reported at their
call site, an error raised inside a callback (map, sort, spawn) keeps its
own deeper trace, and an uncaught throw carries the trace from where it was
thrown. Lex and parse errors name their file too - including the file of a
module that failed to parse, and the import that pulled it in. Very deep traces
are truncated in the middle.
- Runaway recursion is an error, not a crash. Recursion deeper than the call
limit stops with
call stack overflowplus a trace, where it previously overflowed the C++ stack and died on a signal with no message at all. The limit scales with the process's stack limit, soulimit -s 65536genuinely buys deeper recursion, andBEE_MAX_DEPTHoverrides it outright. A numeric function compiled by the JIT recurses natively and so escapes that check; aSIGSEGVhandler catches the overflow and explains it instead of leaving a bare "Segmentation fault" (setBEE_NO_CRASH_HANDLER=1to get the core dump back).
Packages - Hive
hive, a package manager, shipped alongsidebee:
hive install greet # from a registry
hive install ./greet-1.2.0.hive # from a local archive
hive install # everything in hive.json
It resolves dependencies transitively, verifies every download against its
SHA-256, and writes a hive.lock that makes a repeat install reproducible -
and, because the lock carries URLs and hashes, servable entirely from the cache
with --offline. Other commands: uninstall, list, info, search,
init, pack, cache.
A registry is just static files (packages/<name>.json, index.json, and the
archives), so it can live on GitHub Pages, S3, a plain web server, or a
directory on disk. Full documentation in docs/HIVE.md.
-
The
.hivepackage format. A dependency-free container: aHIVE1magic line, a JSON header holding the manifest and a per-file SHA-256, then the file bytes. No zip or tar library on either side, and a corrupt, truncated or tampered archive fails loudly instead of installing. -
hive.jsonmanifests.name,version,main,dependencies(with^,~,>=,<,=and comma-separated constraints),files/excludefor packing, plus the usual descriptive fields. Unknown keys are preserved when Hive rewrites the file. -
A worked example in
examples/hive-demo/: pack a package, install it, import it - no registry and no network needed.
Tooling & tests
-
make testruns two suites:tests/lang_test.sh(56 checks - traces, locations, interpolation, slicing, the REPL, recursion limits, exit codes) andtests/hive_test.sh(44 checks - the package manager end to end, against a registry served straight off the filesystem, so the tests never touch the network). -
The
.deband the Windows installer now installhivenext tobee, andmakebuilds both binaries.
Changed
-
Module resolution finds installed packages.
import namesearches the importing file's directory, its siblinglib/,hive_modules/in that directory and every directory above it,$BEE_PATH, and finally the global library at$HIVE_HOME/lib(default~/.hive/lib). Local code still wins over an installed package of the same name, and a directory found this way is imported as a package via its manifest's"main"(defaultinit.bee). -
Caught errors read better. A runtime error caught by
try/catchbinds as a single line that now includes its location -Runtime error: division by zero (lib/math.bee:8)rather thanRuntime error (line 8): division by zero- and never the stack trace, so printing it inside a message of your own stays readable. -
beewith no arguments starts the REPL (on a terminal) or reads a program from stdin, rather than printing usage and exiting. -
A failed
importof a bare name now suggestshive install <name>.
Fixed
- Reading a module no longer treats a directory whose name matches the module
as an empty source file - the old resolver accepted anything
ifstreamcould open. - Variable references carried no line number, so
undefined variable 'x'was reported without a location. They do now.
Known limitations
- No public registry is running yet, so
hive install <name>needs a--registry(orHIVE_REGISTRY, or~/.hive/config.json) pointing at one. Installing from a local.hivefile works with no registry at all. hive publishdoesn't exist:hive packprints the SHA-256 to paste into your registry metadata.- The REPL has no line editing or history - arrow keys and Ctrl-C aren't handled.
- The VS Code extension doesn't highlight
f"..."strings or slices yet.
0.1.1 - 2026-08-06
Performance work focused on loops, condition checking, and string building. No language or API changes - existing programs run identically, only faster. All 14 examples and the correctness suites pass unchanged.
Performance
- Inline cache for global variables. Reads/writes of top-level (named)
variables now go through a cached pointer instead of a
std::maplookup on every access. Top-level loops that were ~3.7× slower than Python are now ~2.7× faster than before this change. - Cross-function JIT. The native JIT previously handled only direct self-recursion; it now compiles a whole numeric call graph - helpers and mutual recursion - into one module, so those calls are direct and inlinable. A helper-calling loop (e.g. a prime sieve) that used to fall back to the interpreter now runs ~6× faster than Python.
- Top-level loops are JIT-compiled automatically. A numeric top-level
while/forno longer needs to be wrapped in a function to hit native speed. The numeric globals a loop touches are passed as an in/out array, loaded on entry and written back only on clean completion; anything non-numeric (aprint, a string, division by zero) transparently falls back to the interpreter from unmodified state. A 10 M-iteration top-level loop dropped from ~1.84 s to ~0.03 s (~16× faster than Python). - In-place string append. The
x = x + rhs/x += rhsidiom was O(n²) - a fresh, growing string each iteration (~246× slower than Python). It now grows the buffer in place when the string is not aliased, falling back to a copy to preserve value semantics otherwise. Building a 200 k-character string went from ~2.46 s to ~0.03 s (~82× faster). - JIT warmup guard. A small, flat
forloop with a compile-time-known trip count below ~40 k now runs interpreted instead of paying the ~3 ms one-time native compilation, which for such loops costs more than it saves. Nested, large, or unanalyzable loops still compile as before. A 10 k-iteration loop dropped from ~4.3 ms to ~0.5 ms, matching Python. - Void numeric functions no longer run twice. A function that falls off the
end (no value-returning
return) used to be compiled, executed natively, then discarded and re-run in the interpreter - doubling the work. Native completion now reports a nil result directly, so such functions keep their native run. A 100 M-iteration side-effect-free function dropped from ~6 s to milliseconds; a 10 M-iteration accumulating loop runs ~20× faster than Python.
0.1.0 - 2026-08-06
The first public release of Bee - a small, friendly programming language with a built-in native (LLVM) JIT.
Language
- Dynamically-typed values:
nil, booleans, numbers (64-bit float), strings, lists, and dicts. - Variables with
let, lexical scoping, and block/function/loop scopes. - Full operator set: arithmetic, comparison, logical (
and/or/notwith&&/||/!aliases), and compound assignment (+=,-=,*=,/=). - Control flow:
if/else if/else,while, C-stylefor, andfor … inover lists, strings, and dicts, plusbreak/continue. - First-class functions and closures.
- Classes with single inheritance (
extends),this,super, aninitconstructor, and a customizablestr()for printing. - A module system:
import,import … as,from … import, andfrom … import *, resolved relative to the file and a siblinglib/folder. - Error handling with
try/catch/finallyandthrowof any value. - Two comment styles (
#and//) and an optional statement terminator (;).
Runtime & performance
- Compact C++17 runtime with no required third-party dependencies.
- Built-in LLVM ORCv2 JIT (enabled by default when LLVM 17/18 is present):
functions within a numeric subset are compiled to native code operating on
unboxed doubles, transparently falling back to the runtime for everything
else.
fib(32)drops from ~11 s to ~0.014 s. bee --version/-vandbee --help/-h.
Standard library (no imports required)
- I/O:
print,write,input. - Conversion & inspection:
len,type,str,repr,num,int,bool. - Math:
abs,floor,ceil,round,sqrt,pow,min,max,range. - Files:
read_file,read_lines,write_file,append_file,file_exists,remove_file,make_dir,list_dir. - Time & randomness:
clock,time,now,format_time,sleep,random,random_int,random_range,random_choice,random_seed. - Environment & processes:
env,set_env,args,exec. - Threads:
spawn/joinwith a global interpreter lock (GIL) - safe shared state and real overlap for I/O-bound work. - Type methods for strings, lists, and dicts (
upper,split,push,pop,keys,has,get, …).
Tooling & packaging
Makefilebuild with automatic LLVM JIT detection and an overridableVERSION.- Debian/Ubuntu
.debpackage (packaging/build-deb.sh) built with the JIT; dependencies are auto-detected. - Windows installer (
packaging/windows/) via Inno Setup - addsbeetoPATH, associates.be/.beefiles, and registers an uninstaller. - GitHub Actions release workflow that builds both packages and publishes them on a version tag.
Editor support
- VS Code extension (beelang-project/vscode-bee,
then
editors/vscode-bee/in this repo) with syntax highlighting, completions (keywords, built-ins, type methods, and file symbols), hovers, snippets, and a bee icon for.be/.beefiles.
Documentation & examples
- Full language reference.
- A topic-by-topic set of 14 runnable examples.
Known limitations
- The Windows installer ships an interpreter-only build (no JIT yet).
- The
.debrequireslibllvm18, available on Ubuntu 24.04 and newer. - No anonymous/lambda functions - pass named functions (e.g. to
spawn).