Project
Changelog
What changed in each release, newest first, with what is on main for the next one under Unreleased. Built from CHANGELOG.md in the repository; each release's section is also its GitHub release notes.
Each release's section here becomes its GitHub release notes (.github/workflows/release.yml; release-notes.yml re-syncs the notes of an existing release). Benchmark figures are tokens per finished task (code written plus errors read, retries scored as patches), from bench/LEADERBOARD.md. An entry for work by an outside contributor ends with its PR and thanks @handle; the release notes then list everyone thanked, and python3 tools/credits.py checks before a release that no one is missing.
Unreleased#
- Composio example.
integrations/composio_agent.pygives a LangChain agent Composio's hosted MCP server for app tools (GitHub issues here) next tobrevi mcp. Composio holds the credentials and makes the calls; the data reaches Brevi'srunasstdin, so the agent's program runs sandboxed with nothing granted. - Silent wrong output fixed: list items that start with
\x01. On the typed pathprint(["\x01a", "b"])printed['b'], because regex matches kept their group positions as a hidden last string and printing a list dropped any such string. A match is now its own type holding the positions apart, so a list shows every item, printing a match shows<re.Match object; ...>, andm.group(9),m[-1],len(m)andfor x in mfail as in CPython instead of exposing the hidden text. (#200, thanks @timoconnellaus) - CPython conformance measure.
tools/conformance.pycuts each test of CPython 3.13.13'stest_grammar,test_scope,test_int,test_listandtest_dictinto a standalone script and comparesbrevi buildoutput with CPython's: 49.2% of 311 scored tests match (153), 59 are refused and 79 built programs give a wrong answer (51.1% and 73 withBREVI_DYN=1). Report;--checkfails only on a wrong result not incompat/conformance_known_wrong.txt. Not in CI. (#199, thanks @timoconnellaus) install.shverifies its download. The script now checks the release archive against the release'sSHA256SUMSbefore unpacking it, and stops with nothing installed on a mismatch or when the check cannot be made (BREVI_FROM_SOURCE=1still builds from source). (#198, thanks @timoconnellaus)time.gmtime,time.strftimeanddatetime.timezonebuild natively (#194). The dynamic build refused them, so a shared module formatting a UTC timestamp sent 23 of the 27 files of the pipeline in #175 to CPython. Nowtime.gmtime()gives atime.struct_time(fields, indexing,tm_zone),time.strftime(fmt, t)formats it, andtime.time_ns()is there. Datetimes can be aware:datetime.timezone.utc,datetime.UTC,timezone(timedelta(...)),tzinfo=indatetime(...),combine(d, t, tz),replace(tzinfo=...),now(tz),fromtimestamp(ts, tz),utcfromtimestamp,astimezone(tz),timestamp(),utcoffset(),tzname(),%z/%Z, offsets inisoformat(),str()andrepr(), comparison and subtraction as instants, and CPython's error for mixing naive and aware datetimes.fromisoformat("...Z")and"...+02:00"now give aware datetimes; before, theZwas dropped and the result was naive (silent wrong output). Local time (time.localtime,strftimewithout a tuple, a naivetimestamp(),fromtimestampwithout tz) still runs in CPython. Aware datetimes passed to a Python library keep their offset.- A dynamic-build blocker in an imported module names that module's file (#194).
brevi check main.pysaidmain.py line 4: time.gmtime is not supportedwhen line 4 was in_m.py; it now says_m.py line 4, in check, in run's path line and in--explain. (x or "").lower()on a JSON value builds typed (#195).x or ""wherexcame fromjson.loadswas typed as a bool, so the typed build's Rust did not compile and the file took the slower dynamic build. A JSON valueora string or int is now a JSON value. Also:re.search,re.sub,re.findalland friends on a JSON string build typed (they stopped the typed build withno method named re_find found for enum Json).json.load(open(path))builds typed (#195). It stopped check withunknown function __open_r(), which read like an internal error; it now reads the file aswith open(path) as f: json.load(f)does. Other uses of anopen()file the typed build cannot take (passing it to a function, for example) get a plain message instead.- The dynamic build's
reis faster than CPython on search loops (#195). A search now skips straight to the characters a match can start with (also after\b,^or\A, and withre.I), a one-character pattern such as[^a-z ]scans without backtracking, and searching one text with many patterns converts the text once. On a stand-in for the issue's bench (re.search(rf"\b{w}", scene, re.I)over 147 topics and 79 scenes, plusre.sub) the dynamic build went from 0.86x to about 1.8x CPython's speed. - Silent wrong output fixed:
\N{NAME}in.pyfiles.print("\N{BULLET} item")printed the escape as text on the typed path and sent the program to CPython from the dynamic build. Both builds now resolve names (any case, aliases) from a table in the compiler, 400 KB of binary size; a bad name, or a computedCJK UNIFIED IDEOGRAPH-/HANGUL SYLLABLEone, is refused and left to CPython. (#185, thanks @timoconnellaus) - Silent wrong output fixed: float
sum(). Typed builds added floats one by one, sosum([0.1] * 10)printed0.9999999999999999andsum([1e100, 1.0, -1e100])printed0.0. Every typed float sum (lists, generators, JSON numbers, Python library values,sum(xs, start)) is now compensated as in CPython 3.12+ and the dynamic build, andsum([-0.0])prints0.0. (#190, thanks @timoconnellaus) - Uncaught exceptions in Python files exit 1, as in CPython. Native typed builds exited 101, Rust's panic status, and
bench/compare/compare.pyhid it by only comparing zero with non-zero; it now compares exact exit codes..bvprograms still exit 101 after a runtime error. (#187, thanks @timoconnellaus) - A
.pyfile with Brevi-only syntax or methods is refused, as CPython refuses it.if a && b:,x => x + 1,fn f(x):,0..n,true,fail("x")andxs.push(1)on a list used to build and print an answer; nowcheckandbuildreport one error with the Python spelling andbrevi runfalls back to CPython, which gives the real error. A class's ownpushstill works, and.bvfiles keep every Python spelling. (#189, thanks @timoconnellaus) - Silent wrong output fixed: print arguments and tuple items changed by a later one.
print(xs, grow(xs)), wheregrowchanges the list (or a global, orself), printed the list from before the change in typed builds, andr = (q, q.pop())kept a copy ofq; both now go to the dynamic build and print what python3 prints. None of the 580 saved solutions moved off the typed path. (#188, thanks @timoconnellaus) - Silent wrong output fixed: a Python file the guard could not read. When the dynamic parser could not read a file (or a module it imports) that the typed build accepted, such as one using
except*, every guard check was skipped, soprint({3, -1, 5})printed the set sorted. Such a file is no longer built typed:brevi runruns it with python3, andbuildandcheckreport an error. Files with a bareallowline now get the guard's checks too. (#191, thanks @timoconnellaus) - The result cache no longer replays a refusal caused by the machine. When the dynamic build's runtime library could not be built (many builds starting on a cold cache at once, a full disk) or rustc was killed, a Python file was refused with the typed build's reason, such as
cannot work out the type of s, and that refusal was kept and shown again on later runs until the cache was cleared. Such failures are now never kept, and a crate rustc could not load is no longer cached as an error in the program. Seen in the v0.24.6 release run.
v0.24.6 (4 Nov 2026)#
Breaking: sandbox. Under --sandbox, and in the MCP tools, which sandbox by default, a program's own # allow read, # allow write or # allow native line no longer grants anything. Only the caller grants capabilities: --allow read,write,native, --allow-py requests,pydantic (CPython for just those Python libraries), or the MCP tools' allow and allow_py arguments. A sandboxed program whose allow line asks for more than the caller granted is refused. If your setup relied on an allow line in the file, pass the flag or MCP argument instead. Without --sandbox nothing changes: a program with an allow line is still held to it.
A security fix release, out ahead of the weekly Monday release: from v0.24.1 to v0.24.5 code written by an AI (or anyone) could free itself from the sandbox by adding an allow line, for example to reach the network through requests. It also brings dynamic builds that call Python libraries instead of falling back to CPython, and native argparse, textwrap, bytes, hashlib, base64 and a faster native random. Release test over two runs on the release commit: Sonnet 1.03x of Python's cost (95% range 0.97x to 1.09x), Haiku 1.02x (0.97x to 1.08x), every task passed on both sides, no check failed because of Brevi, and 47 of 48 and 48 of 48 final programs native (one Sonnet toc program ran in CPython).
- Dynamic builds call Python libraries instead of falling back to CPython. A Python file that the typed build could not take and that imported a library Brevi lacks (requests, pydantic, yaml, sqlite3, logging, ...) used to run entirely in python3. Now the dynamic build hands just those imports to CPython, which it loads into the program as the typed build's pybridge does, and the rest of the program stays native. Classes built on a library's classes (
class Post(BaseModel)) are run by CPython as a whole, andopen()is CPython's. The path line says what CPython runs:{"path":"dynamic",...,"py":["pydantic","requests"]}. Library values behave as they do in CPython: attributes, calls with keyword arguments, items, operators, iteration (lazy, so generators print in order),with, formatting,isinstance, and exceptions you can catch by the library's class or a builtin base, with their attributes. Functions of the program can be passed as callbacks, and a list or dict a library changes is copied back. A multi-file pipeline modelled on #175 (argparse CLIs, a requests client, pydantic models, JSON files) prints and writes exactly what python3 does.--sandboxstill refuses these imports unless the caller grants them (see the next entry).brevi checkand--nativestay strict. - Under
--sandboxonly the caller grants capabilities. A# allow read,# allow writeor# allow nativeline in the program used to unlock that capability under--sandbox, so code written by an AI (or anyone) could free itself, for example to reach the network through requests. Now the sandboxed program's own allow lines grant nothing: one that asks for more than the caller granted is refused with`# allow native` grants nothing under --sandbox: only the caller grants native. The caller grants with--allow read,write,native, or--allow-py requests,pydanticfor CPython on the named Python libraries only (not rust fn, crates, or other libraries); in the MCP tools these are theallowandallow_pyarguments. Either flag implies--sandbox. Error hints now name the flag to pass, not a line to add. Without--sandbox, a program with an allow line is still held to it. Breaking: a sandboxed run that relied on an allow line in the file needs the flag instead. - Bytes,
hashlibandbase64build natively.b"..."literals were refused by the dynamic build andhashlib/base64ran in CPython; they now build natively (dynamic build), also under--sandbox. Bytes values havebytes(...),str.encode/bytes.decode(UTF-8, ASCII, Latin-1 withstrict/replace/ignore, CPython's error messages), indexing, slicing, iteration,in,+,*, comparisons, hashing and the common methods.hashlibhas MD5, SHA-1, SHA-224/256/384/512 andnew(pure Rust, no crates);base64has b64, urlsafe and b16 encoding and decoding with CPython 3.13's padding rules andbinascii.Errormessages. Fuzzed against python3: 102 hashes over lengths around the block edges, 900 UTF-8 decodes of random byte soup, every base64 edge case tried. Also:repr()of a str in dynamic builds now escapes non-breaking and other separator spaces as CPython does ('\xa0'). argparsebuilds natively. Command-line programs that parse their arguments with argparse (21 of the 27 files in the pipeline of #175) ran in CPython; they now take the dynamic build, also under--sandbox. Brevi builds CPython 3.13's own argparse with the program (tools/mkargparse.py, PSF license), so usage lines,-hhelp, error messages and exit codes are CPython's: 54 runs of three test CLIs (subcommands, groups, choices,nargs,BooleanOptionalAction, custom types,parse_known_args) print exactly what python3 prints. A run takes about 3 ms against 23 ms in CPython.argparse.FileTypeandfromfile_prefix_chars, which open files, still run in CPython. Also:brevi run x.pygives the programsys.argv[0] == "x.py"in dynamic builds, as python3 does;hasattr(value, "__name__")and similar dunder data were True for strings, numbers and None (argparse tests it);raise SystemExit(2)had no.code.textwrapbuilds natively.wrap,fill,shorten,dedent,indentandTextWrapper(all options butfix_sentence_endings) now run in the dynamic build, also under--sandbox, breaking lines exactly where CPython does (hyphenated words, em-dashes, long words,max_linesand placeholders); 1,000 random texts wrapped at random widths print what python3 prints.randombuilds natively outside the sandbox too. Brevi's own copy of CPython's generator now has its core (seeding, draws,getrandbits,shuffle) in native code: a loop of 300,000randintcalls and 100 shuffles takes 0.15 s, against 0.62 s with the old copy and 0.42 s through CPython (CPython itself: 0.11 s). So a program usingseed,random,randint,randrange,choice,choices,shuffle,sample,uniform,gauss,getrandbitsorRandomwith int seeds now takes the dynamic build instead of calling CPython, and draws the same numbers as CPython for the same seed. Other random functions, or a seed that is not an int literal, still use CPython's random.brevi checknames what stopped the dynamic build too. When the typed build could not take a file and the dynamic build refused it as well, check reported only the typed reason (except KeyError: ... and this file cannot be built over dynamic values), so fixing that line still left the file unbuilt and the real blocker (import argparse, a bytes literal) was shown only byrun --explain. The message now ends with it, as(file line N: reason), andbrevi runputs it in its{"path":"cpython"}line. The generator hint no longer suggests moving a yield out of a try block when the yield is not in one. Found running check over a 27-file production pipeline, where every file stopped on one shared line in an imported module.
v0.24.5 (3 Nov 2026)#
A fix release for four silent wrong outputs in typed builds, the ones the v0.24.4 release test found in its trains and poker checks: a list name set to None by a nested function, changes through nested setdefault, joining tuples, and comparing JSON values. On the 813-program corpus 565 programs build typed, up from 550. Release test over two runs on the release commit: Sonnet 1.06x of Python's cost (95% range 1.01x to 1.12x), Haiku 1.01x (0.95x to 1.07x), every task passed on both sides, no check failed because of Brevi (5 did in v0.24.4), and 48 of 48 and 47 of 48 final programs native (the one Haiku cron_next program ran in CPython).
- Silent wrong output fixed: comparing JSON values. In typed builds
j == [](andj == {}) was true for a JSON null, 0, False or"", andFalse == 0on a JSON value was false; both now answer as Python does.v is True/v is Falseon a JSON value is now an identity test (a JSON 1 equals True but is not True), so template renderers that use it build typed. - Silent wrong output fixed: a list name set to None by a nested function.
players = Nonein a nesteddefwithnonlocal playerswas not seen by the outer function'splayers is None, so a typed poker program printed an extra empty round and read later hands as duplicates. The outer test now sees it. - Silent wrong output fixed: nested
setdefaultchanges lost.d = labels.setdefault(s, {}),lst = d.setdefault(k, []), then changinglstchanged only copies in typed builds, so every trip of a typed trains program printed "no route". The change now reacheslabels. - Fixed: joining tuples.
(tm,) + lab[k]gave a list in typed builds (a wrong result, or a TypeError where CPython ran fine); it is now one tuple, also when the right side is held as a JSON value. - Crate pins. The crate test and
examples/checksums.bvnow usesha20.11 andbase640.23. In sha2 0.11 a digest no longer formats with{:x}; the examples build the hex string byte by byte. The docs keepsha2 "0.10", which still works.
v0.24.4 (3 Nov 2026)#
Every one of the 48 final release-test programs from v0.24.3's runs now builds natively, with no CPython: enum, io.StringIO, json.JSONDecoder().raw_decode and path text from os and pathlib run natively, and exact fractions.Fraction, bin/hex/oct, startswith with a start index re.S/re.M and csv.writer(sys.stdout) are on the typed path. On the 813-program corpus 550 programs build typed, up from 496, and eight silent wrong outputs are fixed (re groups and tuples, rows changed in a loop, typed $ and re.split, dict keys printed as None). Release test over five runs (four on main before #171 and #172, one on the release commit): Sonnet 1.06x of Python's cost (95% range 1.03x to 1.10x), Haiku 1.03x (0.98x to 1.10x), every task passed on both sides, 120 of 120 and 117 of 120 final programs native. The run on the release commit alone gave 1.02x and 1.03x. Five checks along the way (of 2 tasks, trains and poker) failed under Brevi where CPython passed the same files; the models fixed their code and passed, and those typed-build wrong outputs are being fixed for the next release. The 3 programs that ran in CPython were split into several files, where a module Brevi could not build (a with other than open, except IndexError) sends the whole program to CPython.
- Typed path round 3. On the 813-program corpus 550 programs build typed, up from 522, none moved off it, and the CPython shadow check finds no wrong output.
j.get(k),j.get(k, [])andj.get(k, {})on JSON values,json.load(sys.stdin),any(f(x, seen) for ...)wherefchangesseen,deps or [], names set inside a loop (or just before itsbreak) and read after it, andreturn Nonefrom a function returning JSON now build typed. AMISSING = object()sentinel is read as None when every test of it also tests None. A loop over a set builds typed when its passes cannot depend on order (adding to sets, appending to the element's own entry, counting, setting a flag). - Silent wrong output fixed: dicts with different keys. When a list held dicts with different key sets (
{"id": 1, "error": "bad"}next to{"id": 2, "total": "4"}), the typed build printed and dumped every key on every dict, so a missing key showed asNone/null. Keys only some dicts have are now left out while unset, as in Python, andk in randr.get(k)see whether the key is set. csv.writer(sys.stdout)on the typed path.w = csv.writer(sys.stdout, lineterminator='\n')(also throughout = sys.stdoutused only for the writer) withw.writerow(row)andw.writerows(rows)now builds typed, quoting fields as CPython's csv module does (a field holding the delimiter, a quote or a line break is quoted, quotes doubled;delimiter=andlineterminator=literals). Of the 48 final release-suite programs, haiku_csv_join moved to the typed path and prints what python3 prints on the task's tests.re.S/re.DOTALLandre.M/re.MULTILINEon the typed path.re.compile(r'\{\{(.*?)\}\}', re.S)andre.split(p, s, flags=re.S), common in template renderers, sent the program to the dynamic build; the flags (and(?s)/(?m)written in the pattern) now build typed. Silent wrong output fixed: in the typed build$matched only at the very end, while Python's also matches before a final newline (re.search(r'z$', 'z\n'));re.splitskipped empty matches (re.split(r'\b', 'a b'), Python 3.7 on splits there) and gave''instead ofNonefor a group that took no part.io.StringIObuilds natively. An in-memory text file (write,getvalue,read,readline, iteration,seek,tell,truncate,print(..., file=buf),with), and socsv.writer(io.StringIO())andcsv.DictReader(io.StringIO(text)), now take the dynamic build, also under--sandboxwithout# allow native. The rest ofio(BytesIO,open) still runs in CPython. Both saved csv_join fallback programs print what python3 prints. Also: under--sandboxthe{"path":"dynamic"}line for such programs namedallow nativethough none was needed; it now says which modules the typed build lacks.bin,hex,octandstartswith/endswithwith a start index on the typed path.bin(x).count('1')and tokenizers that tests.startswith('<->', i)built over dynamic values; they now build typed (negative numbers and negative starts as in Python). Of the 48 final release-suite programs, haiku_sudoku moved to the typed path and prints what python3 prints on the task's tests.json.JSONDecoder().raw_decodeandjson.decoder.scanstringbuild natively. Tokenizers that read a JSON value or string out of a longer text (v, end = json.JSONDecoder().raw_decode(s, i)) ran in CPython; they now take the dynamic build, with CPython's indexes andJSONDecodeErrormessages. These were the last 2 of the 48 final release-suite programs that still needed CPython (bothjson_query); both now print what python3 prints on the task's tests.enumbuilds natively.class Color(Enum)with plain values orauto(), aliases, methods, properties and an__init__taking tuple values, plusColor(1),Color['RED'], iteration,len,in,__members__,@unique, andstr/intmixins (class Level(str, Enum)), now runs in the dynamic build; it used to run in CPython.repr,str, f-strings, hashing and errors (ValueError: 7 is not a valid Color,TypeErroron<) print what CPython prints.IntEnum,StrEnumandFlagstill run in CPython. Also:vars(SomeClass)works, and a class's attributes keep the order its body defines them in.- Exact fractions on the typed path.
fractions.Fractionin a typed build held only denominators that divide lcm(1..32)*10, so every Python file importing fractions went to the dynamic build. A Fraction is now a reduced numerator over a denominator (64 bits each, worked in 128-bit arithmetic), so unit tables, matrix elimination and exact ratios such as 1/86400 build typed and print what CPython prints, includingx.numerator,x.denominator,Fraction("3/4"),//,%,round,absandsum(xs, Fraction(0)). A value that needs more than 64 bits stops with an error naming the limit rather than rounding. - More plain Python stays typed. On the 813-program corpus (bench/typed_corpus.py: saved solutions and repros), 522 build typed, up from 496 on main (386 of the 580 solutions, up from 371); 37 moved onto the typed path and every one with tests prints what python3 prints, and 11 moved off it on purpose (the silent wrong outputs below).
~x,yield [x for ...],(only,) = xs,d.setdefault(k, {})[j] = v,re.sub(pattern, repl, s)withrepla function of the program, a nested function with default arguments, and calls inside a generator inside another generator (" ".join(fmt(x) for x in r) for r in m) now build typed. A re group that took no part in the match is None, as in CPython, som.group(2) is Nonestays typed. - Silent wrong output fixed: re groups and tuples. In typed builds
print(m.group(2))for a group that did not match printed an empty line instead ofNone, andprint(m.groups())showed['a', '', 'c']instead of('a', None, 'c'). A list filled withtuple(cur)by a helper function (out.append(tuple(cur))on a list passed in, or returned) printed its items as lists. All now match CPython (printingm.groups()and such lists goes to the dynamic build). - Silent wrong output fixed: rows changed in a loop.
for r in t[2]: r[1] = von rows held in a tuple (a table kept as(cols, order, rows)) changed copies of the rows in typed builds, so the update was lost; it now changes the rows themselves, and a loop that changes each row while also reading the rows' owner (if test(t, r): r[i] = v) builds over dynamic values. A program whose mixed lists ([1, 'a']) have the same item types as one of its real tuples printed those lists as tuples and copied them where Python shares them; it now builds over dynamic values too. Regex patterns also read\x00,\u00e9,\f,\vand\aescapes, which the typed build matched as letters. - Path text from
osandpathlibbuilds natively everywhere. A program whose only uses ofosandpathlibare path text (os.path.join,basename,dirname,splitext,normpath,os.sep,pathlib.PurePath) used Brevi's own copies of those functions only under--sandbox; outside it the program ran in CPython. It now builds natively in both. A program that also reaches files, processes or the environment (os.environ,os.listdir,pathlib.Path,os.path.exists) still runs those in CPython, as before. - Docs: the site has a Changelog page built from this file, and every diagram is a sharp SVG that opens full size on click; the architecture diagram shows the current build paths.
v0.24.3 (3 Nov 2026)#
brevi check gives its verdict in well under a second, and checking or running an unchanged program again replays the cached result in about 10 ms: the median check of a new file went from 559 ms to 144 ms (60 of 65 programs under a second, up from 40), and a re-check from 60 ms to 8 ms median. Standard-library calls now run natively instead of in CPython (of 47 probe programs, 19 needed CPython before and 5 now), math and time are on the fast typed path (a math.sqrt/math.log loop over 20 million numbers takes 0.09 s, CPython 1.9 s), and two silent wrong outputs are fixed (re.findall with groups, reading a missing defaultdict key). No new release-test run; the v0.24.2 figures stand.
- Faster
check, and repeated calls are near-instant. Agents callbrevi checkandbrevi runover and over, so every call's wait counts. Three changes, measured on 65 programs (the release suite's saved repros and saved solutions; table in the pull request): (1)checkandrunmade all eight versions of the generated program up front, each a whole parse and type-inference pass, even when the first one built or was cached; now the others are made only when the ones before fail. (2) A new result cache:check,buildandrunof files that did not change replay the last result (the same output,{"path":...}line and binary) without parsing, inference or rustc, keyed on a hash of the program's files, the command and flags, theBREVI_*and Python settings, the Brevi binary and the toolchain. A re-check of an unchanged file went from 344 ms on average (up to 3.9 s) to 11 ms, a re-run from 379 ms (up to 4.7 s) to 18 ms. A refusal is replayed only for programs that import nothing an install could change. The MCP server gets this with no change. (3)checkof a file on the dynamic path runs rustc's checks instead of a full build (2.8 s to 0.5 s on one program). Type inference also copies its name tables less.--no-cache(orBREVI_NO_CACHE=1) builds from scratch,BREVI_CACHE_MBcaps the cache (default 1024 MB, least recently used out first; it was 400 entries), andBREVI_TIME=1prints how long each build stage took. Also fixed: a build into a folder that did not exist left its error in the build cache, so building again after creating the folder still failed. - Standard-library calls run natively, not in CPython. A Python file that the typed build could only finish by handing some standard-library calls to CPython (
math.gcd,itertools.product,statistics.mean,copy.deepcopy,datetime.datetime.strptime,functools.reduce,time.perf_counter) now takes the dynamic build, which has those modules natively: no libpython at run time, and it works under--sandbox. Libraries Brevi lacks (numpy, sqlite3) still run in CPython as before. Of 47 small standard-library probe programs, 19 needed CPython before and 5 now (argparse, textwrap, io, os and random; random and textwrap come next); in the 765-program corpus 11 programs moved from CPython to native, and every moved program with tests prints what python3 prints. Also fixed: one missing call such asmath.gcdsent everymath.call on its line to CPython,math.sqrtincluded. - Silent wrong output fixed:
re.findallwith groups. With two or more groupsre.findallreturns tuples ([('a', '1'), ('b', '2')]), but the typed build gave whole matches (['a1', 'b2']). Such calls now build over dynamic values; with no group or one group they stay typed. - Silent wrong output fixed: reading a missing defaultdict key. In Python
d[k]on a defaultdict puts the key in, solen(d), loops and printing see it; the typed build left the dict unchanged. A program that reads a defaultdict with a key that may be missing and also looks at its keys now builds over dynamic values (a key taken from a loop over the dict's keys, or frommin/maxof them, counts as found, so counting and grouping programs stay typed: 1 of the 765 corpus programs moved). mathandtimeon the fast typed path.math.gcd,lcm,comb,perm,factorial(big results grow past 64 bits as in Python),isclose,log(with or without a base),log2,log10,log1p,exp,expm1,sin,cos,tan,asin,acos,atan,atan2, the hyperbolic functions,hypot,fabs,trunc,degrees,radians,copysign,fmod,isnan,isinf,isfinite, the constantspi,e,tau,infandnan, andtime.time,perf_counterandmonotonicnow build typed, whether imported withimport mathorfrom math import gcd, pi. Domain errors are aValueErroranexceptcatches, as in CPython, andmath.sqrt(-1)raises instead of giving nan. A loop ofmath.sqrtandmath.logover 20 million numbers runs in 0.09 s (CPython: 1.9 s). Of the 47 probes,mathandtimeprograms moved from the dynamic build to the typed one; no program in the corpus left the typed path.- Docs: a What runs native table lists, module by module, which functions the typed build has, which run in the dynamic build, and which still need CPython.
v0.24.2 (3 Nov 2026)#
Every build off the fast typed path now says which path it took and which function to rewrite, in one JSON line an agent can act on. More plain Python stays on the typed path: of 765 agent-written programs, 477 build typed, up from 429. random and path functions work in the sandbox, Python files can use # allow read as a comment, fallback errors are shorter (--traceback for the whole one), and printing tuple(xs) no longer shows a list. Release test on this build (one run): 1.09x Python's cost for Sonnet and 1.06x for Haiku, every task passed, 23 of 24 final programs native for each model.
- Every build says which path it took. For a Python file,
brevi run,buildandcheckprint one JSON line on stderr whenever the file is off the typed fast path:{"path":"dynamic","line":3,"fn":"show","why":"print() of a set shows CPython's hash order, ..."},{"path":"pybridge","why":"numpy run in CPython","py":["numpy"]}or{"path":"cpython","line":2,"fn":"g","why":"Brevi could not build it: unknown function eval()","by":"python3"}.line,fn(andfilein a module) name the code to rewrite to get the typed path back. The typed path stays silent;--explainprints it too and adds the fix, up to five more reasons and why the dynamic build refused a CPython run;BREVI_PATH=1prints the line for every build,BREVI_PATH=0never. The CPython fallback'snote: ran with python3; ...line is replaced by its{"path":"cpython"}line. bench/e2e.py logs each pyb task's final path and adds atypcolumn (tasks on the typed path). - More plain Python stays on the typed fast path. On a corpus of 765 agent-written programs (the saved benchmark solutions and past fallback repros), 429 built typed before and 477 now (372 of the 580 solutions, up from 326), none moved off the typed path, and every moved program with tests prints what python3 prints on them. Loops, comprehensions and prints of a set of strings (or of tuples led by one) build typed: CPython draws string hashes afresh each run, so no order is CPython's own (rustc checks the element type; sets of numbers still go to the dynamic build, which follows CPython's order). A comprehension over a set handed straight to
max,min,sorted,any,all,lenorsetno longer counts as an order-dependent use. A sentinelMISSING = object()used only to mark "no value" (d.get(k, MISSING),if old is MISSING:) is read as None when the program cannot tell the two apart (no other None value reaches it, no truth test on it). Also typed now:[[int(x) for x in row] for row in rows]and other comprehensions inside comprehensions that can fail,d.get(k, {})andd.get(k, [])on JSON objects,t[:2]andt[1:]on tuples, a lambda set inside a loop or if body, anddatetime.date(...)in a function with a local nameddate. - Silent wrong output fixed:
print(tuple(xs)). A tuple made withtuple(...)from a list or iterator is held as a list by the typed build, so printing it (or a list of them) showed[1, 2]instead of(1, 2). Such programs now build over dynamic values. - random, path text and
# allowwork in the sandbox. Under--sandboxand in the MCP tools (sandboxed by default),import random; random.seed(1)gave two confusing errors (unknown method .seed()andunknown name 'random'). random now runs natively there from Brevi's own copy of CPython's generator, so a seeded program draws the same numbers as CPython (seed,random,randint,randrange,choice,choices,shuffle,sample,uniform,gauss,getrandbits,Random); any other random function gives one error naming it and saying it needsallow native. Path text no longer needsallow nativeeither:os.path.join,basename,dirname,split,splitext,normpath,isabs,os.sepandpathlib.PurePath(POSIX rules) run without CPython, while every call that reaches files, processes or the environment (os.listdir,os.environ,os.system,pathlib.Path,os.path.exists,ospassed as a value) is still refused, now by name. New tests in tests/sandbox show each of those stays blocked, and that Brevi's own PurePath has no file access at all. In Python files the allow line can be a comment,# allow read, so the file stays valid Python (the bareallow readstill works and stays the form for.bvfiles), and fixes suggest the comment form. - Shorter fallback output. When
brevi runfalls back to CPython on a sure mistake, it printed the note and then CPython's whole traceback, so the error was read twice. It now prints the note and only the traceback's last line, with where it was raised (NameError: name 'totl' is not defined (main.py line 3)).--tracebackorBREVI_TRACEBACK=1shows the whole traceback; other fallbacks are unchanged.
v0.24.1 (3 Nov 2026)#
A hardening release before the public preview. brevi run --sandbox can no longer be bypassed, and the MCP server survives programs that never end or flood their output. Plain Python comes first everywhere: the MCP server builds code as main.py, and brevi check, build and run report sure Python mistakes (an undefined name, a method no type has) instead of building them. Dataclasses and string escapes print as in CPython, programs killed by a signal say so, and the npm wrapper is now named brevi, as on crates.io and PyPI. The README and docs were rewritten for first-time visitors, and brevi-lang.org has a documentation section.
- npm wrapper renamed to
brevi. The npm package is nowbrevi(npx brevi mcp,npm install -g brevi), the same name as on crates.io and PyPI; it wasbrevi-lang. Nothing is published yet. - Newcomer pass before the public preview. README rewritten for a first-time visitor: install, a quick start in plain Python, agents, how it works and the v0.24 status, with the v0.8 to v0.23 result tables moved to bench/HISTORY.md. The site and docs now describe the v0.24 behaviour: sure mistakes in Python files (unknown names and methods, text plus a number) are reported by check and build and named in run's fallback note, a new small program builds in well under a second after a one-time runtime build, and the release-test figures match the v0.24.0 release notes. The skill allows installed packages, release notes give the Windows install step, and examples/top.py is a first Python example.
- Sandbox fixes (security).
brevi run --sandbox x.pycould be bypassed in three ways. A program refused for an undeclaredread()was then run with python3, which read the file anyway (the inner build was also started without--sandbox). Python libraries that CPython runs inside the program (import os,import subprocess, and evensys.modules["os"]through Brevi's ownsys) were not checked at all. And files that go to the dynamic build skipped the check. Now the capability check runs first and its refusal is final on every path. Python libraries needallow native(a Python value reaches everything CPython can, so there is no safe subset), and a program held to its allow lines (--sandboxor anallowline) never falls back to python3. Dynamic builds have no file, network or library access, so a program that passes the check may still build that way; a unit test keeps it so. - Dataclasses print as in Python. In typed builds
print(P(1))for@dataclass class P: x: int; y: int = 0printedP_D { x: 1, y: 0 }instead ofP(x=1, y=0), also inside lists, dicts and tuples. Dataclasses now print CPython's generated repr (fields in order,field(repr=False)left out), a class with__str__still shows its__repr__(or the dataclass one) inside containers and forrepr()and!r, and a class with neither prints<__main__.C object at 0x...>. Dynamic builds also leavefield(repr=False)fields out. - Python first everywhere. The MCP server builds code as plain Python by default (kept as
main.py, built likebrevi run main.py); before, it kept every program asmain.bv, so plain Python failed there (type(it).__name__gaveunknown function ax_type_of(), a class method call gaveunknown method .speak(), andfile: "main.py"on a fresh server said "no program yet"). Brevi code is still recognised by itsfnitems or a.bvfile name.patchreplaces Pythondefs and classes by name (decorators included) and adds new ones after the file's last def. The server's instructions now say to write plain Python and the plain solution, instead of reading the guide first.brevi --helplistsFILE.pyfirst. - Python mistakes are reported, not built.
brevi check,buildandrunon a.pyfile exited 0 on mistakes such asprint(totl(nums))(a name defined nowhere),x.uppr()(a method no Python type has) or"n=" + len(nums): the typed build refused them, the dynamic build accepted them, and the error only showed when that line ran.checkandbuildnow stop with the error and a hint (name 'totl' is not defined,did you mean total?);runruns the file with python3 and names the mistake in its note, so a mistake in a branch that never runs costs nothing. Only mistakes CPython would also stop on are reported this way: a name bound anywhere in the file, a method any built-in type or common module has, a file that catches the error itself, and lines that may never run (under asys.version_info,sys.platformorTYPE_CHECKINGtest, anif False:orx if False else y, or an except block) still build as before (0 of 1,152 saved Python files are refused). No message names Brevi's internal helpers (ax_...) any more. - The MCP server is hardened for the public preview. A program that never ends used to hang the whole server for good: the 20 s limit stopped
brevi, but the program it started kept running and holding the output pipe. Every call now runs in its own process group, and a timeout, a cancel or the client leaving stops everything it started (on Linux children also die with the server). Output is read while the program runs, so a program printing more than 64 KB no longer always timed out; long output is cut in the middle (first 16 KB and last 4 KB kept). The JSON reader is now strict and complete: emoji and other characters sent as\ud83d\ude00pairs (Python's defaultjson.dumps) were dropped, deeply nested input crashed the server, and a line that was not UTF-8 ended it. Batches, non-objects and bad ids get JSON-RPC errors, an unknown tool is a -32602 error, a misspelt or mistyped argument (stdn,timeout: "abc") is reported to the model instead of ignored, and a bad file name (../x.py) no longer breaks every later call. New:notifications/cancelledstops the named call, ping and tools/list are answered while a call runs, protocol versions are negotiated (newest 2025-11-25), tool titles and read-only hints,timeoutonrunandtest,brevi mcp --timeout S --mem-mb MB(Linux: a program gets at most half the machine's memory by default),runwithcasesfalls back to CPython likerundoes (sandbox off), and the work folder and its build folders are removed when the client disconnects (folders left by crashed servers are removed at start). The first call after an install or upgrade timed out while Brevi built its runtime libraries (about 20 s), and kept timing out, since each try was stopped before the build finished; the server now builds them in the background as soon as it starts. When the client's PATH leaves out~/.cargo/bin(Claude Desktop on macOS), the server finds rustc there; when rustc is missing the reply says how to install it. Tested with the official MCP Python SDK and with Claude Code, and tests/mcp_test.py covers each case. - Python string escapes in native builds.
"\x41","\u00e9","\U0001F600", octal"\101"and\a \b \f \vprinted their backslash text instead of the character, andrepr()of a text with control characters showed them raw instead of as\x07; both now match CPython. - Killed programs say so. A program stopped by a signal (out of memory, a crash) exited
brevi runwith 1 and no message; it now prints one JSON line ({"e":"signal",...}) on stderr and exits 128 + the signal.BREVI_MEM_MBlimits a program's memory on Linux. - Docs and new websites. brevi-lang.org has a documentation section (
/docs/): install, using Brevi (commands, flags, errors, CPython fallback, sandbox, GUI test mode), for AI agents (MCP, skill, plugin, llms.txt, the plain-solution advice), architecture with its diagrams, what works and limits, benchmarks, the Brevi language guide, FAQ, roadmap and contributing. Every page is also served as Markdown (/docs/NAME.md). Both sites share one stylesheet, work on phones, and switch their GitHub links and install command on once the repository is public.site/gen.pybuilds the pages fromsite/docs/*.mdand the repo's own docs;bash site/deploy.sh --previewdeploys to workers.dev for review.
v0.24.0 (3 Nov 2026)#
Agents now pay less with Brevi than with Python on heavy work. Told that their Python is compiled to native code, models write the plain solution instead of hand-optimising it; on four tasks with a 10 s limit the whole job cost 0.58x Python's with Sonnet and 0.53x with Haiku, every program built natively and no check failed because of Brevi. On the 24-task release suite, ordinary work, the bar is still met (1.07x Sonnet, 1.00x Haiku). Plain generators and string indexing now build typed and fast, first builds take about 35% less time, and seven bugs found by the new shadow check (each pyb check also runs CPython) are fixed.
- Write the plain version. The skill, the editor rules, the MCP server's text and llms.txt now tell agents that their Python is compiled to native code, which runs plain loops 10 to 50 times faster than CPython, so they should write the plain, direct solution and not hand-optimise it. In a test on the four
speedtasks, this one line cut output tokens by 50% for Sonnet and 28% for Haiku, and cost by 44% and 25%. Every plain program then ran natively within the 10 s limit, where CPython needs 29 to 59 s. bench/e2e.py adds the line to the pyb prompt (--no-hintleaves it out). - Plain idioms build typed and fast. An endless generator read only with
next()(a seeded random number source:def rng(seed): x = seed; while True: ...; yield x) used to send the whole program to the dynamic build. It is now rewritten into a small class, so the program builds typed. The speed set's plain reference programs go from 24 to 29 s to 1.8 to 2.5 s for knapsack, and from over 60 s to 2.3 s for Game of Life (CPython: 25 s and 30 s). Indexing a long text (a[i],b[j - 1]) scanned the whole text each time to check for ASCII; the answer is now kept until the text is freed, so a 10,000-letter edit distance takes 1.8 s instead of 15 s. - Faster cold builds. A build without
--release(whatbrevi runuses) of a program it has not seen takes about 35% less time: the 12 speed programs inbench/comparewent from 405 ms to 257 ms each on average on a 4-core machine (a hello world from 186 ms to 148 ms), and they run as fast as before (all 12 still beat CPython; all 591 saved solutions still match it). Brevi now calls rustc directly instead of through the rustup proxy (the proxy's choice of toolchain is remembered until PATH, rustup's settings or a rust-toolchain file change), uses one codegen unit per core (4 to 16) instead of rustc's 16, looks up rustc while the front end works, and no longer compiles a program twice when the build cache is off (BREVI_NO_CACHE=1, whichbench/compareuses, so its build times there were doubled). - Speed and correctness on fast-Python idioms (found by the new time-limited bench set, where models write optimised Python first). Typed builds: a row taken from a grid (
nrow = new[r]) and changed cell by cell was copied back into the grid after every cell, so a 600x600 Game of Life took 16 s; it is now written back once after the loop when nothing in the loop reads the grid (2.6 s; CPython 30 s). Small runtime helpers (indexing,%,//,+) are inlined into default builds.min(a, b, c)andmaxof three or more values, and built-ins passed tomap(map(sum, rows),map(abs, xs)), build typed instead of falling to the slower dynamic build. Programs that pack many fields into one int (2 ** (F * K),x << (F * w)) build over dynamic values instead of overflowing 64-bit ints. Dynamic builds:&,|and^on ints past 128 bits work (they raised NotImplementedError);bin,oct,hexandint(s, 2)/int(s, 16)on big ints are linear (a 360,000-bitbin()took 11 s);2 ** nis a shift (a bit-packed knapsack went from 20 s to 3 s). Big-int arithmetic borrows its operands instead of copying them (another bit-packed knapsack: 10.5 s to 7.1 s; CPython 6.2 s). - Silent wrong answers fixed.
out.extend(xs)orout += xson a list parameter did not change the caller's list (found by the e2e shadow check in a Haiku poker solution, where it cost 7 checks and the task). A list parameter changed and also returned (xs.append(x); return xs) was not changed for the caller. In dynamic builds,x >> nwith n over about a million bits shifted by a million instead. From the first release-suite runs with the shadow check (19 checks that failed only because of Brevi): a function givingNoneon one path and a list or dict on another (parse(tokens)returning None for bad input) read an empty list as None, so empty input was reported as invalid (such results are now dynamic values; the "may be None or empty" build error is gone).isinstance(r, tuple)was false for a tuple held as a dynamic value.all(c in digits for c in s)andany(...)over a text held as a dynamic value walked nothing, and[c for c in s]over a text gave the text instead of its characters. A list slot set by a nested function, in a list that held an int and None (found = [0, None]), was set on a copy (programs like this now build over dynamic values). Programs usingfractionsbuild over dynamic values, where a Fraction takes any denominator (unit tables stopped with "can't be held exactly"). Dynamic builds adddatetime.time(dt.time(),time(9, 30),datetime.combine(d, t)) andDecimal.scaleb. - Bench. e2e.py's
pybcheck also runs CPython on the same files and logs the pair per check (shadow); the summary'sbugcolumn counts checks that failed only because of Brevi. New setspeed(bench/ref/gen_speed.py): Game of Life, knapsack, edit distance and Collatz chains, small inputs and tens of millions of steps under the 10 s test limit. bench/replay.py replays saved conversations from their first failed check with different feedback (shorter diff feedback made retries 14% cheaper, about 2% of a whole run).
v0.23.0 (3 Nov 2026)#
Closes the last big-int gaps from v0.22.0: // and % now work on big integers, and growing numbers shared between functions (closures, nonlocal, global) grow as in Python. bench/compare still matches CPython on all 591 saved agent programs, 13 of 13 traps and 18 of 18 large programs. brevi build x.py --standalone now puts CPython inside the binary, so a program that uses Python libraries runs on a machine without Python. A new architecture doc explains how a file becomes a native program.
- Standalone builds of programs that use Python libraries.
brevi build x.py --standaloneputs CPython inside the binary: libpython, the shared libraries it and the extension modules need (the C library aside), the standard library, and the installed packages the program imports with everything they require. The binary runs on a Linux machine with no Python installed. It unpacks once into~/.cache/brevi/py(or$BREVI_PY_CACHE), about 1 to 3 seconds on first run, then starts as fast as before. Results stay CPython's own: numpy, pandas, sqlite3/hashlib/zlib, BeautifulSoup with lxml, Pillow, Jinja2 and openpyxl programs, plus one reading argv and stdin, print exactly what python3 prints when run in an empty root holding only the C library. Sizes: about 13 MB with only the standard library, 31 MB with numpy, 42 MB with pandas. Linux only for now. from PIL import Image(a from-import of a submodule the package does not import itself) failed with "module 'PIL' has no attribute 'Image'" in every build that calls Python libraries; it now imports the submodule, as Python does.- The last big-int gaps closed.
//and%work on big integers with Python's floor and sign rules. Examples:fact(50) % MOD, a digit-sum loops += x % 10; x //= 10over a 40-digit number, and-h // 7. Top-level int constants such asMOD = 1000000007join big arithmetic. A growing number shared between functions builds over dynamic values, where it grows as in Python. This covers a closure reading it,nonlocal acc; acc *= v,global total; total = total * 7 + k, and a top-level hash that a function prints. Before, these stopped with an overflow error. - Architecture doc. docs/ARCHITECTURE.md explains, with diagrams, what Brevi does between a
.bvor.pyfile and a native program: front end, typed build, the guard that sends Python to the dynamic build,src/dynpy, library calls through pybridge, rustc and the build cache, and the CPython fallback.
v0.22.0 (3 Nov 2026)#
A bug hunt and a head-to-head with Codon and Nuitka. Four rounds of bug hunting fixed the silent wrong answers they turned up (slices with negative steps, list aliasing on the first build attempt, catchable errors, copy.copy, and more), and the comparison found six more bugs, which are also fixed. On the same unchanged Python files, Brevi builds all 591 saved agent programs and all 18 large programs, and every one prints exactly what CPython prints; Codon builds 206 of the 591 and gets 36 of those wrong. Builds are faster by default: brevi run and brevi build without --release beat CPython on all 12 speed programs (1.7x to 40x). With --release, Brevi is level with Codon where Codon builds, and it also runs the object-heavy programs Codon refuses. Sets and deques in typed builds now give constant-time lookups and popleft(). Ints that grow past 64 bits, including values returned from functions (factorials, Fibonacci, recursion), become big ints as in Python. A growing value captured by a closure or nonlocal still stops with an overflow error.
- Bug hunt, part 1: silent wrong answers fixed (each printed something other than CPython, with no error). Typed build: negative-step slices (
xs[::-2],xs[-1:-3:-1],s[4:0:-1]gave the wrong items or none; anyxs[a:b:k]now picks as Python does, also on text);zs = xsfollowed byxs.sort(),xs.extend(...),xs += [...]ordel xs[a:b]left zs unchanged (now built over dynamic values, where both names share the list); format specs with a fill character or theb,o,g,e,#,_,=and nested{x:{w}.2f}forms ('{:-^5}'.format('a')printeda); f-strings{x!r},{x=},{ x }and{1e16:g}; a plain (not f-) string such as"{x}"was filled in from a variable named x; a list mixing ints and floats printed1000.0as1000. Python files that print or walk a set (the typed build keeps a set in sorted order, CPython walks its hash table:{-1, 5}prints{5, -1}, and a set inside a dict printed as a list), or that change a value inside the print call showing it (print(q, q.popleft())), are now built over dynamic values. Dynamic build:raise ValueError("msg")(any built-in exception) lost its message and args;str.centerpadded odd widths on the wrong side; a set literal of 3 or more constants printed in a different order than CPython. 11 new parity probes cover these. - Bug hunt, part 2. Silent wrong answers: the checks that make Python names share a list (
zs = xs; xs.sort(); print(zs)) were skipped on the first build attempt of every Python file, so most such programs printed an unchanged copy (they now build over dynamic values, where both names share the list); a change through one name followed by setting it anew (xs.sort(); xs = [...]; print(zs)) was also missed.'-'.join("xyz")printed-(nowx-y-z).-0.0 % 5.0printed-0.0(Python's zero remainder takes the divisor's sign), and float or JSON-number%and//by zero gave nan instead of ZeroDivisionError. In a Python file, an unknown method was silently renamed to a near one (s.rpartitionran ass.partition); Python files are never auto-fixed now. Printing arematch showed its groups (['12'],[]) instead of<re.Match object; ...>or None (such files now build over dynamic values). Speed: in dynamic builds everys.add(x),len(s)ands |= tcopied the whole set, so a 40,000-item BFS took 34 seconds; it now takes 0.09 s, as CPython does, and-=,&=and issuperset no longer slow down with the set's size. - Bug hunt, part 3 (exceptions, input, numbers). Python files with
except ZeroDivisionError,IndexError,KeyError,LookupError,ArithmeticError,AssertionError,EOFError,OverflowErrororRecursionErrorbuild over dynamic values; the typed build stopped at those errors instead of letting the except catch them. Silent wrong answers fixed: a dict or set changed in a loop over it went on (CPython raises RuntimeError; such files build over dynamic values);2.0 ** 5000printed inf (OverflowError),0.0 ** -1and(-8) ** 0.5printed inf/nan (ZeroDivisionError, and a refusal since Python gives a complex number); a huge int power wrapped its exponent.input()at the end of the input raises EOFError (it returned "").int("1_000")andfloat("1_0.5")read as in Python.copy.copy(rows)shares the inner lists, as in Python (such files build over dynamic values; the typed build copied them). Dynamic builds: unbounded recursion raises RecursionError (it crashed with a stack overflow), andsys.setrecursionlimitstill allows deep recursion. - Fast sets in typed builds. Typed builds kept a Python set as a plain list, so every
x in sscanned it: a 40,000-node BFS took several seconds and a 400x400 grid BFS with a walls set ran for over two minutes (CPython: 0.03 s and 0.4 s). A list or set that is searched withinand otherwise only added to, popped, counted, sorted or walked (a function's ownseen, or a parameter used that way) is now kept with a hash index, sointakes constant time; the items stay in the same order, so output does not change. The grid BFS now takes 0.8 s and the 40,000-node BFS 0.2 s (what remains isdeque.popleft(), still a list shift in typed builds). Set&and-sort one side once instead of scanning it for every item (15,000-item sets: 2.8 s to under 0.1 s), andwhile q:no longer copies the whole list on every test. tests/typed checks these against CPython with a time limit. - Fast deques and object sets in typed builds. A
deque(or a list taken from the front withpop(0)) that is otherwise only added to, popped, read by index or walked is kept as a ring buffer, sopopleft()takes constant time instead of moving every item (40,000-node BFS: 0.20 s to 0.13 s; the 400x400 grid BFS takes 0.21 s with--release, CPython 0.43 s). Sets stored on objects (self.seen = set(), thenx in self.seen) get the same hash index as local sets when every use of that field is a set use. - Big ints returned from functions. A function whose result outgrows 64 bits returns a big integer, as in Python. This covers a loop that multiplies (
f *= i ... return f), Fibonacci and Tribonacci sums, and recursion such asreturn n * fact(n - 1)orfib(n - 1) + fib(n - 2). These calls stopped with an overflow error before. Where such a call meets other big values or is printed, it stays big. Where an int is needed (an index, a count,//or%), it becomes an int, and raises OverflowError only if it does not fit. A function's locals no longer have to stay small ints just because another function uses the same name. - Fast by default, and the Codon/Nuitka gaps closed.
brevi runandbrevi buildwithout--releasenow compile the program at opt-level 1 against a fully optimised runtime, so the default build beats CPython on all 12 speed programs inbench/compare(it was slower on 5): trees 9.8 s to 0.71 s (CPython 5.2 s), strings 5.6 s to 1.13 s (2.4 s), records 2.5 s to 0.38 s (0.71 s), nbody 1.4 s to 0.12 s; a cold build takes about 0.3 s longer. Typed programs get a small-object allocator (per-thread free lists for blocks up to 256 bytes, as CPython's pymalloc does), about 25% faster on string and dict work, plus quicker one-charactersplitand{i:06d}formatting. List slice assignment (xs[a:b] = ys,perm[:k+1] = perm[k::-1]) builds typed, so fannkuch runs at 3.5x CPython instead of 0.4x. Ints that grow past 64 bits in Python (a factorialf *= i, a rolling hashh = h * 31 + ord(c), Fibonaccia, b = b, a + b, big literals) become big ints,3 ** 200 % mis worked out exactly, and a literal power past 64 bits builds over dynamic values; these stopped with an overflow error before (a value returned from a function still does). Generators work in dynamic builds and run lazily, step by step with their caller as in CPython (endless generators,send(),yield fromwith a return value,StopIteration.value, generators in classes and closures); the typed build, which runs a generator to the end at its first use, keeps only generators where that cannot show (no prints, no reads of outside state, no while loops), and a yield inside try/finally or with is refused at build time.csv.DictReaderis native (rows are dicts of str, sod[r["dept"]]no longer guesses int keys),__file__is the file's absolute path, and thesys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))line needs no CPython.csv.reader(f, delimiter=";")is no longer read with commas. The chess huge-set program with generators and both__file__programs now build natively and match CPython. - Brevi vs Codon vs Nuitka (
bench/compare/): the same unchanged Python files built by Brevi, Codon 0.20.6 and Nuitka 4.3.4, checked against CPython's exact output and exit status, with build and run times. Of 591 saved agent solutions, Brevi builds 589 and matches CPython on 588, Nuitka 591 and 591, Codon 206 and 170 (16 of its builds print a different answer with no error). On typed code Brevi--releaseruns 2.7x to 47x faster than CPython on 9 of 12 speed programs, level with Codon; Nuitka stays near CPython. It found four silent wrong answers in Brevi, now fixed:book = asks if buy else bidsfollowed bybook.remove(o)changed a copy (such files now build over dynamic values);m.group(3) is Nonewas False for a group that did not match (likewise); an f-string value starting with a number (f"{1/3:.3f}",f"{2*n}") printed as literal text;raise SystemExit(3)exited with 0 (dynamic build) or 1 (typed build) instead of 3. Also fixed two wrong refusals:d.setdefault(k, set()).add(x)stopped with KeyError, and10 ** -2stopped with an error instead of giving 0.01.
v0.21.0 (2 Nov 2026)#
Brevi passes its release test: over four runs each with Sonnet and Haiku (e31 to e34, each side's prompt cache kept apart), every task passed, plain Python built by Brevi cost 1.10x CPython with Sonnet (right at the 10% bar) and 0.95x with Haiku, and all 48 final programs built natively. Python files the typed build cannot handle now build natively over dynamic values instead of falling back to python3, and Python libraries (numpy, pandas, sqlite3 and any other installed module) run inside native builds through CPython loaded at run time.
- All 18 saved solutions of the huge set (chess, adventure and an interpreter, 200 to 750 lines over several files) now build natively and match CPython.
- Note: the dynamic build path compiles its runtime once into
~/.cache/brevi/dynrt, so the first such build on a new machine takes about 30 seconds longer. - Release test passed (runs e31 to e34, 2 Nov 2026): over four runs each with Sonnet and Haiku, every task passed (48 of 48), plain Python built by Brevi cost 1.10x CPython with Sonnet and 0.95x with Haiku (the bar is within 10%), and 48 of 48 final programs built natively (the bar is 90%).
- Python files that the typed build cannot handle now build natively over dynamic values instead of falling back to python3. Values whose type changes at run time (a list mixing numbers, text and None; an interpreter's tuple AST; JSON rows) work as in CPython, with CPython's printing, ordering (sets, dicts), int/float/big-int arithmetic, hashing, exceptions and tracebacks.
re,json,decimal,fractions,datetime,csv,statistics,html,collections,itertools,functools,heapq,bisect,dataclasses,copy,math,operator,stringandthreading(the stack-size trick) work in these builds; anything else (exec,open,os, a module function Brevi lacks) is refused when the program is built, so the CPython fallback still runs it. Forcing every one of the 580 saved Python solutions through this path (BREVI_DYN=1), all 580 print exactly what CPython prints on every test. In the normal build (typed first), all 8 release-test fallbacks from e27 to e30 and all 18 saved huge-set solutions (chess, adventure, minilang) now build natively and match CPython on every test. Dynamic builds are faster than CPython but slower than typed builds (1.3x CPython's speed on a dict and recursion micro benchmark, 4.5x on the minilang tests counting process start), so the typed build is always tried first. - Python: a state list mixing a list with plain values (
[board, 'w', 'KQkq', -1]) is a record; the big-program chess solution that failed now passes (all 6 chess solutions build).pat == (4, 1)compares a list made bytuple(...)with a tuple literal. A mixed list stored in a list of dynamic values prints with[ ], not( )(it printed as a tuple before). - From the e27/e28 trials (every task passing, 19 of 24 native): a bound method passed as a callback (
orders.fill(sku, stock.take), thentake(sku, n)inside); a parameter named like a module (def fill(self, sku, stock), thenstock.take(...)) is the parameter, not the module; a loop oversorted(m.get(k, []), ...)that changes its items no longer raises KeyError for a missing k;min(d)/max(d)on a dict give a key; a name set to tuples of different lengths in separate places (c = (t, a, b), laterc = (n, path)) is a new name each time when each setting is only read after it;al = run.get(k)takes the entry's type;sc = g.scores[name]thensc[q] = vtypes the field's inner dicts. All 5 e27/e28 fallbacks now build natively and pass every test. - Python libraries in Python files:
import numpy as np,pandas,sqlite3,subprocess,pathlib,urllib.parse,hashliband any other installed module now run inside the native binary, through CPython loaded at run time (libpython). Their values, methods, operators (includinga @ b), iteration, comprehensions and exceptions (except sqlite3.OperationalError) behave exactly as in Python. Brevi's own modules stay native; what they lack (math.gcd,d.strftime(...)on a date) is taken from CPython, learned from the build's errors. A list passed to a library sees its changes (random.shuffle(xs)), and lambdas work as callbacks. Before this, any such import madebrevi runfall back to python3. 12 of 12 realistic library programs now build and match CPython (they used to fall back), and 25 of 32 short library snippets do. The other 7 are older native gaps, now with the roadmap. - Dynamic values (a parameter or slot whose type could not be fixed) get the text and list methods Python code uses: slicing,
lower,upper,strip,split,startswith,endswith,replace,find,isdigit,isascii,sorted(..., key=),reversed,sep.join(xs)and more (e25/e26 fallbacks). Fixed silent wrong output:s[i]on dynamic text gave None (so a time check likes[2] != ":"was always true); it now gives the character, and an index past the end of dynamic text or a list raises IndexError. - From the e24 trial: a function kept under another name at the top level (
_old_end_call = end_tx, called inside a function defined above it); int arithmetic and comparisons with a value read from a mixed dict (self.total(n) * 100 // self.max, checked to be an int when it runs); a dict record whose list key onlysetdefaultmakes (cur.setdefault("msgs", []).append(m), thenr.get("msgs", [])). Before, that last one would have printed the messages as missing. Sonnet's books, quiz and poker attempts from e24 now build natively and pass every test. - Huge set:
_, frm, d, to = cwith c a dynamic tuple gives dynamic values (they were unknown, so a field filled from them was never typed); a dict keyed by tuples with a dynamic slot is read, tested and set with plain keys (self.exits.get((self.loc, d)), also through a key kept in a name); a dynamic value appended to a list of str is checked to be a str; a dict keyed by dynamic values is read with a plain key. Haiku's h1 adventure (the saved solution) builds and matches CPython on all 9 tests; 12 of the 18 saved huge solutions now build. - Fixed wrong output:
Fraction(1, 3) + Fraction(1, 6)printed 0.499999999 (a Fraction was a 9-place decimal). A Fraction is now exact for every denominator that divides lcm(1..32)*10 (thirds, sevenths, 1/64, 1/1000 ...), prints as1/2(Fraction(1, 2)inside a list), floors with//as Python does, and stops with an error naming the value rather than round one it cannot hold (1/41). @lru_cache/@cache(alsofunctools.) on a function memoises it, as in Python; before, the decorator was dropped, so a memoised recursion such asfib(80)ran in exponential time. Methods and*argsfunctions still drop it.json.dumpsof a literal with lists or dicts of mixed values (json.dumps({'a': [1, 2, {'b': None}], 'c': True}, indent=2)) builds; it was refused.- From the e25/e26 trials: a name inside a comprehension no longer takes the type of the same name in a later loop (
[pt(x) for x in tk[3::2]]next tofor i, (nm, x) in enumerate(st)made pt's parameter dynamic); a list or dict literal with a None or empty slot (heap = [(start, 0, (), frm, None)],{(frm, t, None): []}) takes that slot's type from what is put in later; a name set from a function that returns a dict record (cur = new()) gets the keys set on it later (cur.setdefault("out", []).append(m)). a dict record kept in a one-slot holder or under a key (cur = [None],cur[0] = {...};state['cur'] = {'bad': True}next to the full one) is one record, also throughtx = cur[0]andt = state['cur']; a row pushed as[name, {}]is a fixed-slot row (a tuple), androws[i][1][q] = vtypes its dict. With the dynamic-value methods from #111, all 8 e25/e26 fallbacks (trains, books, quiz, poker) now build natively and pass every test. - Fixed a refusal added in #103:
b, turn, rights = statefollowed byb = list(b)and changes to b was refused as a copy that cannot be written back (Sonnet's h2 chess); b is a new list there, so nothing needs writing back.
v0.20.0 (2 Nov 2026)#
Plain Python now always runs: brevi run x.py builds a native binary when it can and otherwise runs the file with python3, with a one-line note naming what stopped the build. Python objects, rows and records are shared by reference as in Python, values without one fixed type become dynamic values, error classes are real classes, files can be read and written with open(), and tkinter programs build (headless for tests, or a real window with --window). The parity probe of short Python programs shows no silent differences from CPython. On the release test, measured the fair way (runs e25 and e26, right after this release with #104 added, each side's prompt cache kept apart; every task passing), plain Python built by Brevi costs 0.99x CPython with Sonnet and 0.98x with Haiku. 16 of 24 final programs built natively (67%; it varies by run, 39 of 48 in e23 and e24; the bar is 90%), so the release test is not passed yet. Earlier runs (e15 to e24) let CPython reuse the prompt cache Brevi's runs had paid to write, which counted against Brevi (those runs showed 1.04x to 1.08x). brevi-lang.org leads with AI agents again: the fast, safe target for AI-written Python.
- brevi-lang.org, llms.txt and the README put AI agents first again: "The fast, safe target for AI-written Python." The agent writes plain Python; Brevi checks it, answers mistakes in one line and builds a native binary.
- Decimal
a // banda % b(also with an int) in Python files, with Python's Decimal signs (toward zero;%follows a). Fraction uses the same rule, which differs from Python for negative values. - Benchmark: each e2e run tags its prompt, so
pybandpythonno longer share a prompt cache. Until nowpybran first and paid the cache writes thatpythonthen read. - From the e22/e23 trials: a name set to a list in one loop and to a tuple later (
c = [...], thenc = (a, k, l)) is a new name each time, as in Python (Sonnet's trains); a method's arguments are typed where the call is (name = t[0]theng.has(name), before a laterfor name in g.names), and calls on a value known to be a class count (Sonnet's quiz);a, b = curfills the tuple's slots from how a and b are used later (Sonnet's poker); adefinside anifat the top level;sc = {},self.students[name] = sc, thensc[q] = vin a loop (the storing is moved after the loop when nothing reads the dict in between). 8 of the 10 programs that fell back to CPython in e22 and e23 now build natively and pass every test. - Fixed wrong output:
players, dealt = curfollowed byplayers[k] = vdid not changecur(now written back, as withp = cur[0]);cur is Nonewas true aftercur = Noneand latercur = ({}, set()), since Brevi keeps None in a list or dict name as an empty one (such a name now carries a flag; where it cannot, as for a parameter tested again afteracc = [], the program is refused); a list stored in a dict and then changed by aforloop whose body is one call (for c in cs: row.append(c)) lost the change (now refused). - Huge set (minilang): a dict of functions set once and only called by key (
CMP = {'<': operator.lt, ...},CMP[op](a, b), or lambdas) picks the function in place, and a missing key raises KeyError;operator.lt,operator.addand the like as values; a parameter that defaults toprintand is never passed (def __init__(self, funcs, out=print)withself.out(text)) prints. A name inside a generator (fmt(self.ev(e, loc)) for e in s[2]) no longer takes the type of anexcept RErr as eelsewhere in the function (Sonnet's minilang). - From the e22 trial (raising the share of programs Brevi builds natively): a list started as
[]is never taken for a dict (counters[i] = 0aftercounters.append(0), Haiku's toc); a sort key that can fail (xs.sort(key=wline)withint(...)inside) stops the run on a bad value;set(d)of a dict is the set of its keys (Haiku's trains); a list or dict read at a dynamic index (CATS[key[0]], Haiku's poker); a method's parameter waits one round longer for its calls before it is taken as a dynamic value. A loop over a method's list of objects that is the last statement of an if branch (else: for bo in orders.waiting(sku): bo.remaining -= got) no longer gets a copy refusal: the objects are shared (Haiku's warehouse). Huge set: dicts with different keys pushed on one list (stack.append({'kind': 'if', ...}),{'kind': 'while', ...}) are one record; tuples of different lengths in one list are dynamic values;_, frm, d, to, key = cunpacks a dynamic tuple of 4 to 6 items (a wrong count stops the run);dict(v)copies a dynamic dict; a dict literal mixing a dynamic value with others is a dynamic dict;w.capacity = c[1]checks the value is an int. - Python:
re.compile(...).match(v)andfindallwork whenvholds values of mixed kinds (a str checked withisinstance). print(..., file=sys.stderr)(withsep=andend=),sys.stderr.write(s),sys.stdout.write(s)andprint(..., file=sys.stdout)in Python files.- More of minilang (huge set): a syntax tree grown as nested tuples (
l = ('add', l, self.term())), token lists that mix('int', 2)and('sym', '+')(that slot holds dynamic values),v in '+-'andv in ('==', '!=')on such a value, awhile True:loop that leaves only byreturnat the end of a function, error classes defined in another file of the program, andx in (a, b, c, d, e, f)with up to 8 items. A whole number past 2^53 in a dynamic value stops the run instead of losing digits. - Python error classes with fields of their own (
class RErr(Exception): def __init__(self, msg, line=None)) are real classes:except RErr as egets the object raised, soe.line,e.msgande.line = n; raisework, andstr(e)is its message (a form Brevi cannot match stops the run).threading.Thread(target=f, args=(..))with.start()/.join()runs f in place;threading.stack_size(n)andsys.stdout.flush()are accepted. A method named like a built-in (def args(self)) works, and a dynamic value passed to an int or str parameter is checked when the call runs. Fixed: a class without constructor defaults took those of the class before it (B(n)was givenA's defaultline=None). - Fixed wrong output: a Python
trywith severalexceptclauses ran the first one for every error, soexcept Brk: break/except Cont: continuebroke out of the loop on a continue. Each clause now catches only its classes (and their subclasses,class NotFound(AppError)), an error no clause names goes on up, and a bareraisein a handler raises the same error again. A failed built-in call is caught by any built-in error name. - Python values without one fixed type are dynamic (JSON) values, as in the huge set's minilang: an untyped parameter called with different kinds, a function that returns different kinds by path, a name set to tuples of different shapes,
type(c) in (list, str)andtype(a) is type(b),int(s)/float(s)on such a value (a bad string raises ValueError), andw.rooms if c else w.itemsused inx in d. A list typed from its first items that later gets mixed items is returned as a mixed list. - Reading and writing files in Python files:
open(p).read(),open(p).readlines(),for line in open(p),with open(p) as f:(also several at once, withencoding=),f = open(p)...f.close(), and writing withopen(p, "w")or"a"throughf.write(s)andprint(..., file=f). A missing file raises an error thatexcept FileNotFoundError:catches.s.splitlines(True)keeps the line ends. A.pyfile always gets Python rules, even when nothing in it looks like Python. - The last silent difference in the parity probe is gone:
[True, None]printed[True, False]; such a list is now held as mixed values and prints as Python does. In Python files, the str + int error suggests an f-string (f"...{x}"), not Brevi's"{x}". - brevi-lang.org leads with speed and safety: write plain Python, get a native binary (1.3x to 10.8x faster than CPython on the speed set, measured 2 Nov 2026), with whole-program checks before running. The old "up to 21% fewer tokens" claim is gone (it came from the earlier code-and-errors count; end to end, plain Python built by Brevi costs about what CPython does). llms.txt points agents to the skill first and marks the guide as .bv-only.
brevi run x.pyruns the file with python3 when Brevi cannot build it, and says so in one line on stderr (note: ran with python3; Brevi could not build line 5: unknown method .permutations()). Plain Python always runs; when Brevi can build it, it runs at native speed.--native(orBREVI_NATIVE=1) keeps the strict behaviour, andbrevi buildandbrevi checkstay strict.BREVI_PYTHONpicks the interpreter. The benchmark'spybcheck does the same and logs whether each task built natively (--nativefor the old strict check).- From the e20 trial: a stub that only returns None (
def parse_number_key(line): return None);q.update(type='choice', letter=x)on a dict used as a record (one key set per argument); a record field that starts as0and has Decimal amounts added (state['deb'] += a) holds a Decimal;dict(base, type='choice', answer=a)with base a dict set once from plain names (each shape is its own record);sorted(q['answer'])on a mixed record's set or list value. From e21: a list built withcounters = counters[:d],counters += [0] * n,counters[d - 1] += 1is no longer taken for a dict; a top-level list of tuples with a slot that is only ever None (players.append((name, sc, None))), filled throughglobal players. Type checking a function with many empty lists and dicts no longer takes time that doubles with each one (a 364-line solution took 90 s to check, now 5 s). - Huge set: a function that returns tuples of different lengths by path (
('room', name, desc),('capacity', n)or None), with plain int, str, bool or None items, now works: callers readc[0],c[3],len(c), print it as Python does, and add its whole-number items to an int (anything else stops the run). Tuples with a float item are still refused, since a float could print as an int. - More of the huge set:
(p[1], p[3]) + tuple(p[5:])joins a tuple with a list (the result prints as a tuple);d = rooms if kind == 'room' else itemswith two dicts of different value types, when d is only used inx in d/x not in d;up = (lambda s: s.upper()) if by == 'w' else (lambda s: s); a function whose body is onlypass. Fixed wrong output: printing the result of a Python function that gives no value now showsNone, not(). next(gen, None)gives None itself when nothing matches, sox is Noneis true afterwards (it gave""or 0, and a lateris not Nonetest went the wrong way: the e20 Haiku poker attempt).Decimal.scaleb(k),int(s, base)(with0x/0o/0bprefixes and base 0) andsys.stdin.readline(). "Did you mean" hints only name a near miss now:sys.stdin.readline()was told "did you mean .re_find()?" andint(s, 16)"did you mean input()?". Thebrevi mcpguide tool now says plain Python needs no guide first.- Python results that silently differed now match CPython:
-7 % 3is 2 anddivmod/ float%follow the sign of the divisor;round(2.5)is 2 (ties go to even); None in a float list printsNone; a set prints as{1, 2}(orset());math.isqrt(n)is the whole square root (the auto-fix used to swap it forsqrt). Reading a missing key of a plain dict raises KeyError, as in Python; adefaultdictorCounterstill reads zero.range(a, b, k)in a loop is counted through directly, so a sieve runs about 9x faster than CPython instead of 3x slower. - Huge set: types that are only partly known (
state = (board, 'w', None)from one function, the None slot filled by another) now flow between functions, so the known parts type the parameters they reach. A class with its owncopy()method gets that method instead of the list copy. A constructor parameter passed None, whose field is set to an int later, takes the field's type.P, N, K = "PNK"unpacks a string;c in (R, Q)tests a tuple of like items; a function that returns a value on some paths and can run off its end returns None there. Haiku's h1 chess builds and passes 10/10.dict(v)andv.copy()on a dict record give a new record with the same values. - Silent wrong output from the parity probe is now refused with a fix:
b = aon a plain list or dict followed by a change through one name and a read of the other (Brevi copies; Python shares),[[0] * n] * m(Python rows are one shared list), and a mutable default argument (acc=[]) that the function changes. '%d: %s' % ewith the tuple in a name (or a field or item), as in Sonnet's h2 adventure world file.json.dumps(x)in Python files, withindent=,separators=,sort_keys=andensure_ascii=, writes what Python's json module writes.d.get(k, default)andk in dwork on JSON read withjson.loads, also with a key that is itself a JSON value. From the saved-solutions sweep (16 solutions stopped atjson.dumps).- Also from e19: Python's one-item box (
cur = [None], read and set only ascur[0], also inside nested defs) is a plain name the nested defs share; a dict record field that starts as[None] * 9gets its type from what is put in it later (the g2 tic-tac-toe). Fixed wrong output:(gone if old else keep).append(l)now appends to the list the condition picks instead of to a copy. - The agent skill (
skills/brevi/SKILL.md, also in the Claude Code plugin) now tells agents to write plain Python and run it withbrevi run main.py, with no guide to read first; Brevi syntax and the guide are for.bvfiles only. Same-day runs on the xl3 set (r1): writing Brevi cost $0.270 (Sonnet) and $0.046 (Haiku), plain Python built by Brevi $0.190 and $0.028. tools/parity_probe.pyruns short plain-Python probes (tools/parity/probes.py) with CPython and withbrevi runand lists each one where Brevi prints something else (silent wrong answers) or refuses what CPython runs. First run: 17 silent differences and 24 refusals in 71 probes (missing dict keys, negative%,round, None in printed lists, list aliasing, mutable default arguments).- Regex match positions:
m.start(),m.end(n),m.span(), andre.finditer(p, s)/pat.finditer(s); a bracketed import over several lines (from grade import (a, b,thenc)). From the e19 trial. - From the e19 trial: a lambda given a name and only ever called (
bad = lambda n: out.append(...)inside a function, orsq = lambda x: x * xat the top) is a nested or top-leveldef, so its parameters are typed from the calls and it can change the outer function's lists. A top-level name that starts as None and is later set to a tuple of empty lists and sets (cur = ([], set(), set()), filled throughcur[0].append(x),cur[1].add(n),cur[2].update(xs)) takes its type from those uses, also when functions read it throughglobal cur. - A comprehension split over lines inside braces (
{k: f(q)thenfor q in qs}on the next line, or anifon its own line) parses, as in Python. From Haiku's e18 quiz first attempt. xs.count(x)on a list (it used to need a closure) andmin(xs, default=d)/max(xs, default=d), also withkey=f, from the e18 trial.date(*map(int, s.split("-")))anddatetime.date(*parts): a date from spread year, month and day.- tkinter apps built around a class work: a method as a callback (
command=self.inc,canvas.bind("<Button-1>", self.tap),root.after(100, self.tick, n - 1)) runs on the object itself, now that Python objects are shared. Haiku's tic-tac-toe from theguiset passes every test. itertools.groupby(xs, key=f)(runs of neighbouring items with the same key) anddel xs[a:]/del xs[a:b], from the e17 trial.repr(x)(Python's text for a value, e.g."a'b"or[1, 2]) anditer(xs)(sonext(iter(s))takes the first item).- From the huge set: a function whose tuple has a None slot (
return ([1, 2], 'w', None)) takes that slot's type from the other function whose result goes in the same name (s = new_state(), thens = step(s, k)); a tuple that is still None printsNone; a list set to[]in one branch and to a comprehension in the other takes the comprehension's type; andtype(v) is int/type(v) == booltest a value's exact kind. Fixed wrong output: a list taken withdq = lots.get(sku, deque())and changed by a helper (take(dq, q)that pops lots) now changes the stored list. 409 of 580 saved Python solutions passbrevi checkunchanged, 343 build, and 342 of those give the right output on every test. - Python lists of mixed kinds used as rows (
backs.append([oid, sku, qty])) and dicts made records are now shared too, like objects: a row changed through a filtered copy, a picked item (max(rows, key=...)) or another name changes everywhere. The "copy, not shared" errors now come only when the value really is a copy (a list of plain numbers or strings, a dict used as a dict); the Rust compiler checks the value's type at each such place. Haiku's GUI to-do app and the e17 warehouse first attempts that changed shared rows or objects now build. Fixed wrong output (pre-existing):top = max(grid, key=...)thentop[1] = 0on a list of int lists is now refused instead of silently changing a copy. - Python objects are now shared, as in Python: an object made from a class is a handle to data that lives for the whole run, so every name, list slot, dict value and field holding it sees the same object (
alias = st.lots[0]; alias.qty += 10,for l in st.for_sku("a"): l.qty -= 1,drain(st.last, 2)). A None object is a null handle, and reading a field of one stops with Python's'NoneType' object has no attributeerror. Lists and dicts are still copied when they are passed or stored. A method call that passes an object and reads its fields as arguments (self.set_shown(self.shown + k)) builds. A local named after a field (t = self.tx, thent['postings'].append(p)orself.discard(t)) now changes the field too, or is refused when later lines also set or use that field. Fixed wrong output: Haiku's z2 books solution, which built for the first time with this change. A class named like a Python built-in (class Counter) is the class, notcollections.Counter. 410 of 580 saved Python solutions passbrevi checkunchanged, 345 build, and 344 of those give the right output on every test. - A Python list of mixed literal kinds that is looped over (
vals = [1, 'a', True, None, [2, 'b']],for v in vals: show(v)) is a list of JSON values, so a helper taking each item withtype(v) is int/isinstance(v, list)checks builds (huge-set repro r2). - From the GUI set's tkinter apps: a list of None filled in later (
board = [None] * 9, thenboard[c] = pin a function); walking a tuple of one type like a list (all(board[i] == p for i in line),for i in line,i in line,sum(line));d.update(k=v, ...)andd.update({...})on a dict used as a record. Fixed wrong output: None inside a printed list of strings printed as' None'. - Fixed wrong output: a Python list that mixes kinds (
[oid, sku, n]) now prints as a list, not a tuple (unless the program also has real tuples of exactly those types). Changing an item of a list a helper returned as a filtered copy of another list (vis = visible_tasks(), thenvis[i][1] = ...) is now refused rather than silently changing a copy; Python would change the shared item. 410 of 580 saved Python solutions passbrevi checkunchanged, 344 build, and 343 of those give the right output on every test. - More plain Python from the e17 trial's first attempts: a field filled as a map of maps (
self.scores[name][qid] = score); a dict key set later to values of different kinds in different branches (q['spec'] = t[2],set(t[2])or a list) holds any of them; dicts a function returns from different branches (return {"Name": name, "N": n, "big": True}) are one record, also when some values are parameters; and capitalised keys. Now refused instead of guessed: a dict name set both to None and to{}and then tested withis None, since Brevi can't tell those apart yet. Every e15, e16 and e17 first attempt that builds prints what CPython prints. - More plain Python from the e16 trial's first attempts:
if obj:on a class instance (true unless None, or by__bool__/__len__) and on a date; a dict that starts with a None value next to numbers (state = {"accepted": 0, "last_date": None}) or with only empty collections ({"players": [], "names": set()}) becomes a record whose keys get their own types; dict values from calls ({'name': n, 'total': total(xs)}) type as records; a class method named like a built-in (stock.take(sku, n));for (s, y) in dover tuple keys;all(xs[i] for i in ...)by truth; andout = print. Fixed wrong output: a loop over a field that changes its items while also changing other fields of the object (for b in self.backs: b[2] -= got; self.shipped += got) now changes the stored items, a method that only changes the items of its object's list now changes the object, and a None object is told apart from one whose fields are all zero. Changing the items of a sorted or filtered local copy (cand = sorted(l for l in self.lots if ...), thenfor l in cand: l[1] -= n) changes the originals, or is an error where Brevi can't do that yet. Built e16 first attempts now all print what CPython prints. - Python dicts made records can get keys later in branches (
q = {"id": qid, "type": typ}, thenq["answer"] = afor one type andq["value"] = Fraction(v)for another); such a key prints only once set, and'answer' in qon one is refused rather than guessed. A helper that returns a filtered list is now written back also when called with an item (for b in od.waiting(w[1]): b[2] -= got). The e15 Haiku quiz first attempts (both runs) now build and print what Python prints, and Sonnet's e15a warehouse passes every test. brevi build --window(andrun --window) builds a tkinter program with a real window, drawn by egui through theeframecrate (downloaded and compiled once, about a minute, then cached). Without a display, or withBREVI_GUI=headless, the same binary runs headless, so tests still work.- Python tkinter programs now build. Brevi compiles the widgets,
StringVar/IntVar/BooleanVar/DoubleVar,pack/grid/place,command=callbacks (function names, lambdas,lambda ch=ch: ...in a loop),bindwith the event (e.char,e.keysym,e.x),after/after_cancel(alsoafter(ms, f, x)), canvas items, listboxes andmessageboxdialogs onto thebrevi_tkruntime, which is built into the program (no crate to download). Built programs run headless for now:mainloopreads test actions from stdin and prints the window, exactly as the CPython stand-in does. Programs written with top-level functions work (Sonnet's to-do app in theguiset passes every test); programs built around a class (self.on_clickas a callback) wait for Python object sharing. A lambda's default values (lambda k=k: ...) also work outside tkinter. Other GUI libraries (pygame,PyQt, ...) still give a clear "not supported" error. - GUI benchmark set
gui(bench/ref/gen_gui.py): a to-do app, a calculator and tic-tac-toe in tkinter, tested headlessly.bench/tkshimis a headless tkinter stand-in for CPython that prints the window the same way as Brevi's GUI runtime (tools/gui_shim_check.sh). Plain Python passes all three first try with Sonnet and Haiku; Brevi cannot build tkinter programs yet. - String literals written next to each other are joined, as in Python, also across lines inside brackets:
("abc "\n "def"),f"{n} " f"items". - New held-out benchmark set
huge(bench/ref/gen_huge.py): an interpreter, a chess referee and a text adventure, 200 to 750 lines over 3 or 4 files. First runh1: plain Python passes 3/3 with Sonnet and Haiku; Brevi-built Python 2/3 with Sonnet (2.1x the cost) and 0/3 with Haiku. See bench/README.md. - Python programs split over several
.pyfiles now build:import stockandfrom shop.store import Storefindstock.pyorshop/store.py(next to the importing file, then next to the main file, as Python does; folders with__init__.pyare packages). A module's ownif __name__ == "__main__":self-test is left out when another file imports it. Importing a GUI library (tkinter,pygame,PyQt, ...) now says plainly that Brevi has no GUI support yet, instead of a confusing type error. - More plain Python from the e14 trial's first attempts: a lambda called at once (
(lambda s: (s[0], s[1]))(score(t))); rows of a mixed list changed in a loop (backorders.append([oid, sku, left]), thenfor b in backorders: b[2] -= got); dicts built from a method's parameters (self.lots.append({"seq": n, "sku": sku, "qty": qty})) and dicts with a key that is a keyword ('type') become records; and a loop that changes the dicts a helper returns after filtering and sorting them (lots = [l for l in self.lots if ...],lots.sort(key=...),return lots) changes the stored dicts, as in Python. The e14 Sonnet poker and warehouse and Haiku warehouse first attempts now pass every test, and Haiku's quiz builds and prints what Python prints. - More plain Python from the e15 trial's first attempts: dicts built from local names and filled in key by key (
cur = {...}thencur["k"] = v) become records, including keys that are keywords; a dict whose keys are set in a loop (memo[k] = v) is typed as a dict, not a list;re.I/re.IGNORECASEflags;math.floor/ceilon decimals; a lambda's nested tuple unpacking (for (a, (b, c)) in ...); and a helper that filters a local copy of a list and returns it writes changes back. Wrong output turned into errors: a loop at the end of a function that changes copies, removing items from a list while looping over its indexes, and a list that may be None or an empty list ([] if ok else Nonethenis None), which Brevi can't yet tell apart. The e15 Sonnet poker and quiz, Haiku warehouse and Haiku trains first attempts now pass every test. Fixed wrong output:h, a = teams[home], teams[away]followed byh['P'] += 1now changes the stored dicts (Haiku's y2 league). 409 of 580 saved Python solutions passbrevi checkunchanged, 342 build, and 341 of those give the right output on every test.
v0.19.0 (1 Nov 2026)#
Brevi now aims to build plain Python unchanged ("Python in, Rust speed out"). The release test is a plain-Python trial: models write ordinary Python, Brevi builds it, and the full API cost is compared with running the same models' Python on CPython. In run e14 Brevi costs 1.16x Python with Sonnet and 0.94x with Haiku, from 2.2x and 1.8x this morning. 331 of 580 saved Python solutions build and give the right output unchanged. The release test is not passed yet.
- From the e14 retries:
{d:+.2f}keeps the plus sign on a Decimal, Python'sstringconstants work (string.digits,from string import ascii_lowercase, and the same for uppercase, letters, hexdigits and octdigits), and JSON values can be dict keys and set items. - The release test is now the plain-Python trial (
e2e.py --lang pybagainst--lang python, same day, Sonnet and Haiku, two runs). It passes when Brevi-built Python costs no more than CPython and passes as many tasks. See bench/README.md. - Plain-Python trial
e14(model writes .py, Brevi builds it): Sonnet 1.16x the cost of plain Python (2.2x in e10 this morning), Haiku 0.94x. See bench/README.md. - Python values whose type differs from row to row now work. This is the quiz pattern: a question's answer key is a letter, a set of letters, a
(value, tolerance)pair of decimals or a list of words, depending on the question type. Such a value holds JSON, whether it is a slot of the returned tuples (also nested,return qid, ('multi', pts, frozenset(s))), a class field given different types by different calls (Question(qid, kind, pts, data)), or a name set to different types in the branches of an if (spec = parts[3]/spec = frozenset(s)/spec = (Decimal(v), tol)). Where such a value meets a decimal or a list it is read as one:abs(v - spec[0]) <= spec[1],s == spec,s & spec,ans in spec,value, tol = q.data. Alsoa <= b/a < bon sets as a subset test,all(words)/any(xs)on strings and numbers (by truth),\Zand\Ain regexes, fields namedtype(q.type),Decimal(a) / Decimal(b),Fraction(Decimal(s)),s.casefold(),order, questions = [], {},a, b = s.split("=", 1)inside a loop, andbest = Nonewith 5- and 6-tuples. Fixed wrong output:dq = lots.get(sku, deque())(or[]) followed by changes to dq now changes the stored list, as in Python. Every quiz first attempt from the e11, e12 and e13 trials now builds and prints what Python prints. 404 of 580 saved Python solutions passbrevi checkunchanged, 334 build, and 331 of those give the right output on every test. - Python import aliases:
from datetime import date as Date,import datetime as dt,from collections import Counter as C(for modules Brevi has built in).datetime.timedelta(...)works liketimedelta(...), and a date that is still None printsNonein Python files. Together these fix Haiku's failed books task in e13. - Clearer build error when rustc can't work out a variable's type: it names the variable and says whether it starts as None or as an empty collection, instead of the general "type annotations needed". This rustc error is no longer shown when a more specific one is also reported. Run
e13of the plain-Python trial is in bench/README.md. - More Python classes from the e12 trial:
self.lots.setdefault(sku, []).append(lot)types the field as a dict of lists,for k in list(self.lots):goes over the dict's keys, a loop that changes the items a small helper method returns (for lot in self._ordered(sku):withreturn sorted(self.lots.get(sku, []), key=...)) changes the stored items,dealt.update(cards)types a set from a list, andst = stats[k]/dd = nxt.setdefault(k, {})followed byst[0] += qordd[t] = xtypes the entry, and a name set on some passes of a loop keeps its value into the next pass (if key != prev: pos = i + 1thenprint(pos); this gave wrong ranks before), andif best:works on a tuple that started asbest = None. The e12 warehouse and trains first attempts (and e11 sonnet trains) now pass every test. 403 of 580 saved Python solutions passbrevi checkunchanged, 327 build, and 324 of those give the right output on every test. - Python dicts used as records (
tx = {'n': n, 'date': d, 'posts': [], 'bad': False}: fixed keys, values of different types) become record types, the way a class does.tx['posts'].append(p),tx['bad'] = True,tx.get('k', d)and'k' in txwork; dicts put in the same name with fewer keys (cur = {'silent': True}) join the record; records print as the dict and go into JSON as an object. Also: a class's fields filled from outside the class (shelf.items.append(x)on a top-level object), a helper that returns an item of a dict (st = stat(sku)withreturn stats[sku]) changes the stored item, andm = months.setdefault(date[:7], [0, 0])with a computed key writes back. The books first attempts from the e11 and e12 trials now pass every test. 398 of 580 saved Python solutions passbrevi checkunchanged, 318 build, and 315 of those give the right output on every test. - Python loops that change the items of a sorted or copied list (
for lot in sorted(lots, key=...),for b in list(backorders)that may alsobackorders.remove(b)) change the list's own items, as in Python: the loop goes over indexes and each change is written back. Records written as lists of mixed types (lots.append([expiry, seq, sku, qty])) are tuples with settable slots (lot[3] -= n), Python'sxs.remove(x)takes out the first item equal to x, andc = d.setdefault(row[2], [0, 0])thenc[0] += nchanges the stored list. - Python lists that hold values of more than one type (a tokenizer's
tokens.append(int(s[i:j]))next totokens.append(c)) hold JSON values, and arithmetic, comparisons,abs,//, unary minus andinwork on them as in Python; JSON values print as Python prints them ([1, '+', 2],None,True). Alsof'{"#" * n}'(a value starting with a quote inside a single-quoted f-string) and\at the end of a line to continue it. 309 of 580 saved Python solutions now build unchanged, and 305 of those give the right output on every test. - Python code can set a name again to a value of another type in the block that made it (
w = int(w)on a loop name,st = int(st)afterip, st, path = m.groups()): from there on the name holds the new type, as in Python. Also unpacking a list into up to 12 names (ip, ts, meth, path, proto, st, size, ref, ua = m.groups()), anddist.get(v, INF)withINF = float('inf')on a map of ints. 292 of 580 saved Python solutions now build unchanged (260 before). - More Python:
frozenset(xs),m.pop(k, default),s - kon sets made withset(...)in a function whose types are only known later,x = Nonefor 4-tuples, and dates inside a mixed-type dict. - Python's unlimited ints: in a Python-style program, a number read with
int(text)that is only added, subtracted, multiplied, compared and printed becomes a big integer, soa, b = int(p[0]), int(p[1])/print(a + b)works for 60-digit numbers (all 24 savedbigaddPython solutions now give the right output; they hit a runtime error before). Any other use keeps it a fast int. Alsore.splitwith groups gives the groups' text too,x = m.setdefault(k, [0, 0])shares the stored value as in Python, and Decimals inside lists and dicts print asDecimal('1.50'). With the parser work, 247 of 251 built Python solutions now give the right output on every test (py_as_brevi.py --full --run). - A function that returns a JSON value on some paths and a number on others (
if isinstance(x, (int, float)): return x...return 0) now returns JSON, andsum(f(v) for v in xs)over such results works, so the usual recursive JSON-walking code builds unchanged. - Python code that relies on shared objects no longer gives wrong output silently.
for a in m.values(),for k, a in m.items()andfor i, x in enumerate(xs)that change the item now change the original. Where a change would still be lost (changing items ofsorted(lots)in a loop, changing a list after putting it in another, changing an item picked bymax(...)),brevi checkrefuses the program and says how to write it. Also fixed wrong output from plain Python:range(a, b, -1)counted nothing,if x:was true for a None number or string,(x,)was taken as x,m.setdefault(k, 0)alone on a line did nothing, and/inside an f-string's{...}was whole-number division.bench/py_as_brevi.py --full --runnow also runs each built solution on its task's tests: 224 of 248 built solutions give the right output on every test (188 of 247 before these fixes); most of the rest need Python's unbounded ints (int("123456789012345678901234567890")), which stop with an error rather than a wrong answer. - More Python from the plain-Python trials: module-level values built by comprehensions or calls (
R = {c: i for i, c in enumerate(s)},dict(pairs),[(c, 1) for c in s]) can be read inside functions; nested functions that call each other (expr()callingpeek()) pass along what the callee reads from the outer function;def __len__is whatlen(x)calls anddef __str__/__repr__is how an object prints; a parameter is typed from calls inside generators (all(valid(c) for c in hand)); a loop name that is still unknown no longer hides an earlier setting of the same name;tuple(xs)works (kept as a list); andm.get(k)on a map of maps or lists gives an empty value when k is missing. - Python names share objects, and Python-style programs now follow that:
r = rows[k]orr = resting.get(rid)followed byr[0] -= t, oracct = accts[name]followed byacct.bal += 5/acct.deposit(5), changes what the container holds. Before, the change was silently lost, giving wrong output. Brevi-style programs keep their copies. Also nested unpacking ((a, b), c = ...,for bid, (r, d) in m.items()in comprehensions), Brevi keywords used as Python names (def var(...),match = ...,type = ...), lists filled byheapq.heappushtyped inside nested functions and changed there (heappopin a nested function changes the outer heap),x.quantize(...)typed as a decimal, and printing of 4- and 5-tuples. - Python code that reads JSON works more often unchanged:
json.loads(s)andjson.load(sys.stdin),isinstance(v, int)andisinstance(v, (str, list))(on a JSON value it checks what the value holds; on other values it follows the type, with bool counting as int as in Python),v is None,v == 3andfor x in v/sum(f(x) for x in v)on a JSON value. 355 of 580 saved Python solutions now passbrevi check --fastunchanged (334 before). A function that returns a JSON value on some paths and a number on others is not supported yet. Alsox = Nonethat is later a Decimal (printsNoneuntil set), andFractionas an annotation. - More Python from the plain-Python trials:
for ... else:andwhile ... else:,lots[:] = [...](the caller's list changes),not 1 <= mo <= 12,y, mo, d = (int(x) for x in s.split("-")), a docstring at the top of an imported file, quoted and loose annotations (-> "int | None",-> object,c: list = [0] * 3),pl = pd = Nonewith one name set later, tuple constants used as lists (SIZES = ("S", "M"),for s in ("a", "b"):),()as an empty list, lambdas that unpack rows (key=lambda r: r[1]on lists of lists), and Python printing for scripts with nodef. A file defining the same function twice no longer makesbrevi checkhang. - More Python from the guide trials:
nonlocal xin a nested function changes the outer function's x;m.group(3) or ""and othera or b/a and bon values that are not bools give a value, as in Python; a parameter a function never reads (stops: listpassed along) is dropped instead of failing to type;for k in m:over a dict reads its keys; and a method of a class from another file, or with a built-in name (board.add(name, row),stock.take(sku, n)), is called once the value's class is known, with its changes kept.p[1]on pairs from a top-level list filled underglobalworks inside functions. - More Python:
d.to_integral_value()on a Decimal (half to even),m.setdefault(k, v)as a statement with any default,defaultdict(lambda: 0)(and other empty defaults),enumerate(xs, start=1),bool(x), andsys.setrecursionlimit(n)(accepted; native code needs no limit). 334 of 580 saved Python solutions now passbrevi check --fastunchanged (321 before). - More of Python's library:
s.update(xs)on a set,m.update(other)on a dict andc.update(xs)on a Counter;datetime.date(y, m, d),datetime.strptime(s, "%Y-%m-%d")(.date()gives the date),timedelta(days=n, weeks=w)added to a date,d.isoformat(),d.toordinal(),d.weekday()(0 = Monday in Python-style files) andd.isoweekday();calendar.monthrange(y, m)andcalendar.isleap(y);format(x, spec);float(d)on a Decimal;s.isascii(); andFraction(s)/Fraction(a, b)as exact decimals (printed as decimals, not3/10). These were the library calls behind failed builds in the newe2e.py --lang pybtrial, where the model writes plain Python files and Brevi builds them. - More Python:
s.ljust(n),s.rjust(n, c),s.split(sep, n),s.rsplit(sep, n),list(s), plusset(s)ands.count(x)on a string whose type only Rust knows, anda - bon sets. - Python type annotations are accepted wherever Python allows them and used where they help:
self.names: list[str] = []andself.best: Optional[Lot] = Noneon fields,best: intwith no value,list[tuple]and other part-typed annotations (left to inference),Decimal,Union[...],int | str,Callable[...],Iterator[...]. Models write these all the time when they write Python, and they were the most common error left in the Python-first guide trial. Alsox in ("a", "b"),m.get(k)(None when missing),sum(xs, start), and None printing asNone. - Faster Python library calls in native builds:
for row in csv.reader(src)reads rows one at a time with a faster parser (2M rows: 2.8 s and 514 MB before, 0.47 s and 36 MB now; CPython 0.87 s),Counter(s.split())counts without building the word list (9.6M words: 1.3 s to 0.32 s; CPython 1.4 s), and regexes match runs like\S+in a loop andre.matchtries only the start (300k lines: 0.79 s to 0.45 s; CPython 0.37 s).csv.readergives[]for a blank line, as Python does. - More Python:
xs.clear()andm.clear(),sys.argv,sorted(m, key=f)on a dict, anddate(y, m, d). - Python programs split over several files work unchanged:
import stock/from stock import Stock, parsereadsstock.bvnext to the file (lib.stockislib/stock.bv),stock.parse(x)andimport stock as scalls work, and a used file may holddef,class, decorators, imports and module-level constants. This was the main cost of the multi-file tasks in the Python-first guide trial. - Generators:
yield xandyield from xs(the function collects what it yields, sofor v in f(),list(f())andsum(f())work; an endless generator is not supported).f"{3.14159:.2f}"and other format specs on a number written in place,"-" * 40,Nonefor a date, andtry: return f(s)/except ...: return Noneat a function's end. - More Python classes and calls work unchanged:
@dataclassclasses (fields fromname: strlines, with defaults andfield(default_factory=list)), inheritance (class Bird(Animal):gets Animal's fields and methods, andsuper().__init__(...)runs Animal's), Python type annotations (list[str],dict[str, int],Optional[T],int | None,List/Dictfrom typing), default and keyword arguments (def scale(x, k=2),scale(5, k=3)),*args, and a class's own method named like a built-in one (stock.add(4)next toseen.add(3)). In Python-style programs/always gives a float (7 / 2is 3.5) and an int mixed with a float becomes a float, as in Python. Alsoa, b = line.split()inside a function,dict(m)andBird("x").describe(). - Python classes work unchanged:
class Account:with__init__and methods becomes a type with the fieldsself.x = ...sets, typed from what is put there;Account("ann", 10)anda.deposit(5)call them. Error classes (class BadInput(ValueError):) may have a body. Also"""docstrings"""and other triple-quoted strings,a ** b,0xFF/0o17/0b101,fnas a name, andst[-1], st[-2] = st[-2], st[-1]. - More plain Python is accepted unchanged:
try/except/else/finallyandraise(a failing call orraiseinsidetryruns the handler, withas egiving the message), error classes such asclass BadInput(Exception): pass,pass, nesteddef(lifted out, with the names it reads passed along),;between statements (also inif c: a; b), import lines anywhere,def main()withif __name__ == "__main__":, raw strings,return a, b,a = b = 0,h, m = s.split(":")from a list,print(*xs), dict and set comprehensions,{a, b}sets with|and&,<<,>>,^,|=and the other compound operators,x is y, and comprehensions over a dict's keys. With the library batches (#32, #33, #34), 217 of 580 saved Python solutions passbrevi check --fast(9.7% before), and 134 (23.1%) build fully. With the class, import and generator batches below, 282 (48.6%) pass and 166 (28.6%) build. - Types are optional everywhere. A parameter without a type now also takes the type its own function's uses show (
lots.push(Lot(...)),seen[id] = true,st[t],p.qty), anddate(s).daysandweekday()are typed. With every annotation removed from the 48 saved held-out XL solutions, 46 still build and pass (31 before). The guide now says so and its examples drop their types. - A parameter changed in place (
xs.push(x),xs[i] = v,p.n += 1) edits the caller's value withoutmut, as in Python. It stays a copy when the function sets it whole or returns it. Leaving outmutused to give silently wrong output. global xandnonlocal xare accepted. A program that uses them follows Python's scope rule exactly; programs without them keep Brevi's rule.- Python's
f"..."andf'...'strings are accepted (Brevi strings already fill in{name}). - A program's own type named
Resultno longer clashes with Rust's. - A top-level map filled inside a function (
ts = {}, thents[n] = Team()in a function) is typed where other functions read it, sofn key(n: str) = (-ts[n].w, n)needs no return type. This was the most common first-check error in recent runs. inner = m.get_or(k, {})is typed by what is done with it, like a fresh{}, so nested maps built this way at the top level work inside functions.- A function ending in
if c: m.remove(k) else: print(...)gives no value, as in Python. - Functions are values:
ops = [add, mul], pairs such as("+", add), and calls straight from a list, tuple or field (ops[0](3, 4)). - A third held-out set of six XL tasks (
bench/ref/gen_big5.py, runs z1 and z2). Brevi is still behind Python: Sonnet 9,042 vs 7,990 tokens (13% more), Haiku 12,405 vs 11,394 (9% more), from 19% and 4% on the second set. Brevi passes first time more often with Haiku (10 of 12 vs 9 of 12), but its finished programs are no shorter than Python's at this size; type annotations are about 5% of Brevi's tokens. s.index_of(sub)ands.index(sub)on strings (-1 when missing).Nonefor int, float, str and tuple values:best = None,x is None,x is not None,return None.- Maps of functions:
ops = {"+": add, "-": lambda a, b: a - b}, thenops[op](a, b)orfor k, f in ops.items(). - More Python habits:
set(xs)ands.add(x),xs.extend(ys),xs[::-1],divmod,round(x)/round(x, n),enumerate(xs, 1),m.setdefault(k, []),defaultdict(int),"%d %s" % (a, b),"{:>5}".format(x),math.sqrt,sys.stdin,[0] * n,p.0.1, andimport sys/from collections import ...lines. These came from compile errors in earlier benchmark runs. - Python 3.15 unpacking in comprehensions (
[*xs for xs in xss]) reads as a comprehension followed byflatten()(#15). Rust 1.99 builds cleanly (#13). - The core guide is trimmed from 5,035 to 3,490 tokens; rare details move to REFERENCE.md, served by
brevi guide WORD, and CI fails when BREVI.md goes over 3,600 tokens (#18). - Install channels prepared for launch, not yet published: Claude Code plugin and marketplace, MCP registry
server.json, a PyPI wrapper, an npm wrapper (brevi-lang) and a dataset export of 49 tasks with 1,893 verified solutions (#20). - Grammar for constrained decoding:
grammar/brevi.larkand a llama.cpp copy,grammar/brevi.gbnf, which accept every test program and all 1,370 verified Brevi solutions. Examples for LangChain, the OpenAI Agents SDK, CrewAI and the Vercel AI SDK overbrevi mcp(integrations/), and a Homebrew formula generator (#22). - Safe to run:
allow read, write(ornative) at the top of a program declares what it touches,brevi run --sandboxrefuses anything undeclared, and the MCP tools sandbox by default (sandbox: falseturns it off). - A second held-out set of six XL tasks (
bench/ref/gen_big4.py). On it Brevi is behind Python: Sonnet 10,997 vs 9,222 tokens, Haiku 11,851 vs 11,426 (#19). - Faster builds: the runtime prelude is compiled once per rustc version into a cached library instead of being rebuilt with every program (a build goes from about 0.73 s to 0.36 s,
brevi checkfrom 0.55 s to 0.21 s), and builds are cached by a hash of the generated code, flags and rustc version, so running the same program again takes about 0.1 s. Programs are built without debug info (5 MB to 1.1 MB). The compatibility check of 1,971 old solutions drops from about 6 to under 3 minutes.BREVI_NO_CACHE=1andBREVI_NO_RTLIB=1turn these off. brevi mcpkeeps the program between calls: sendcodeonce, then change it withpatch(whole functions by name) or the newedit(a range of lines), which check again in the same reply, so a retry costs the changed lines instead of the whole file. Multi-file programs work throughfile.runtakescases(stdin and expected output), builds once, and printsokor the first differing line per case.showprints the kept program with line numbers. Calls that passcodework as before.- Sorting by a key (
xs.sort_by(f),sorted(xs, key=f)) works out each key once, as Python does, instead of twice per comparison.--releasebuilds keep the prelude inside the program so its functions can be inlined. - An end-to-end benchmark,
bench/e2e.py, counts every token and dollar the API bills per finished task (guide, thinking, tool calls, retries). On held-out XL set 3 Brevi costs 58% more than Python with Sonnet and 78% more with Haiku, mostly in model output; see bench/README.md. - More of Python's library works unchanged:
rstrip,lstrip,strip(chars),splitlines,isupper,islower,isalnum,heapq(heappush,heappop,heapify,heappushpop,heapreplace) on a plain list,dequewithpopleft/appendleft,map(f, xs),filter(f, xs),list(xs),map(int, xs)andkey=len,sorted(s)of a string, andsys.stdout.write. The share of saved Python solutions Brevi accepts unchanged (bench/py_as_brevi.py) goes from 9.7% to 13.1%. - Python's
Decimal(x)andd.quantize(q, rounding=...)work; adecnow remembers how many places it was written with (1.50, or what quantize set), which Python-style printing shows.csv.reader(sys.stdin)andcsv.reader(lines)give the rows, anddate.fromisoformat(s)isdate(s). Values that print in Python's style (True,2.0,['a'],{'k': 1}) are ready in the runtime for programs written as Python. - More Python:
next(rows),xs.pop(i)andm.pop(k)(which give the item back),sorted(s),enumerate(s, 1)andsep.join(s)on a string,sorted(m)on a map,(d2 - d1).dayson dates, andOrderedDict(). In Python modeint("q")fails with Python's message. OrderedDictas an LRU cache:move_to_end(k)andpopitem(last=False)/popitem().- Python's
remodule:re.compile,re.match,re.fullmatch,re.search,re.findall,re.sub,re.split, compiled-pattern methods, andm.group(n)/m.groups().next(it, default)andnext(x for x in xs if c). - The build cache now includes the runtime's hash and the brevi version, so a changed runtime (after an upgrade) never reuses an old build or error.
- More Python:
zip(xs, ys)(also with a string),Counter(xs)withc.most_common(n),dict(pairs),sys.exit(n)andsys.exit(msg),s.discard(x),s.count(sub),zfill,title,center,capitalize, and{c for c in s}on a string. A comprehension'sif i % 2uses Python truth. A program withimportorfrom ... importlines now prints the Python way (True,['a'],{'k': 1},4.0), as programs withdeforclassalready did.
v0.18.0 (31 Oct 2026)#
Closes most of the gap on the six XL tasks (150 to 400 lines, two split over several files): Sonnet 12,630 vs Python 13,128 tokens (4% fewer), Haiku 15,908 vs 15,658 (2% more), from 8% and 13% behind. Brevi was tuned on these runs.
- Python's
sorted(xs, key=f, reverse=True),min(xs, key=f),lambda, format specs such as{x:.2f}, andprint(x, end="", sep=", "). failused like Python'sraise, with onematchat the top.- Rows and nested fields edited in place (
for r in rows: r[i] = v,books[id].wait.push(x)), also while the loop reads the same table. - Global maps filled through a copy inside a function; a function's own local may share a top-level name.
return 0in a function that otherwise ends on a print;while True:with a return inside;(_, x) =in branches; names reused across sibling blocks;n.range().- Prebuilt binaries for Linux (x86-64, ARM), macOS (Intel, Apple silicon) and Windows, downloaded by
install.sh.
v0.17 (31 Oct 2026, no separate release)#
- Libraries:
use "util.bv"for multi-file programs,use sha2 "0.10"withrust fnbodies for Rust crates, andpy fnwith Python bodies for any installed Python package. See LIBRARIES.md. - Sonnet passes first time on 22 of 24 big tasks (39 of 48 in v0.15); Brevi is ahead of Python on the eight big tasks for both models (Sonnet 8,284 vs 8,721, Haiku 9,398 vs 10,556).
"{{"and"}}"as a method's first argument,j.int()/j.float()on JSON strings,s.find(sub, start),s.char_at(i), untyped parameters typed from their calls, and a program's ownfn min,fn argsortype Ord.- The MCP
checktool can return the fixed program (fix: true). - Daily toolchain checks on Rust stable, beta and nightly and the newest and next Python, rerunning every benchmark solution that ever passed (#2).
v0.16 and earlier#
- v0.16:
brevi mcp(local MCP server),brevi check --fast,brevi patch,brevi guide WORD, Agent Skills package, AGENTS.md, llms.txt, install.sh, Dockerfile. - v0.11 to v0.15: Brevi's own type checker before rustc, exact token counts in the benchmark, template strings, Json reads.
- v0.4 to v0.10: the core language (then called Axon): Python-style blocks and spellings, implicit error passing, ownership inference, heaps, decimals.