Skip to content

Releases: ets-public/brevi

Brevi v0.25.0

Choose a tag to compare

@github-actions github-actions released this 04 Nov 14:00
0f08089

The launch release, out ahead of the weekly Monday release. Most of it is correctness: Brevi now measures itself against CPython's own test suite (tools/conformance.py), and the fixes it led to cut built programs that give a wrong answer there from 79 to 45 while the share of tests that match rose from 49.2% to 57.9%; every other silent wrong output found in the release runs is fixed too, including d.copy() and list(d) on a dict in typed builds. 567 of 813 saved agent programs now take the fast typed build, with none differing from CPython. Python files exit 1 on an uncaught exception, Brevi-only syntax in a .py file is refused as CPython refuses it, install.sh checks its download against SHA256SUMS, and the dynamic build's re beats CPython on search loops. Release test, two runs on the release tree: Sonnet 1.09x and Haiku 1.01x the cost of CPython, every task passed, no check failed because of Brevi, 95 of 96 final programs native (two earlier runs on the tree just before #207: 1.04x and 1.05x, 96 of 96).

  • Composio example. integrations/composio_agent.py gives a LangChain agent Composio's hosted MCP server for app tools (GitHub issues here) next to brevi mcp. Composio holds the credentials and makes the calls; the data reaches Brevi's run as stdin, so the agent's program runs sandboxed with nothing granted.
  • Silent wrong output fixed: list items that start with \x01. On the typed path print(["\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; ...>, and m.group(9), m[-1], len(m) and for x in m fail as in CPython instead of exposing the hidden text. (#200, thanks @timoconnellaus)
  • CPython conformance measure. tools/conformance.py cuts each test of CPython 3.13.13's test_grammar, test_scope, test_int, test_list and test_dict into a standalone script and compares brevi build output with CPython's: at first 49.2% of 311 scored tests matched (153), 59 were refused and 79 built programs gave a wrong answer; after the fixes below from that run, 57.9% match (180), 67 are refused and 45 give a wrong answer (57.6% and 45 with BREVI_DYN=1). Report; --check fails only on a wrong result not in compat/conformance_known_wrong.txt. Not in CI. (#199, thanks @timoconnellaus)
  • install.sh verifies its download. The script now checks the release archive against the release's SHA256SUMS before unpacking it, and stops with nothing installed on a mismatch or when the check cannot be made (BREVI_FROM_SOURCE=1 still builds from source). (#198, thanks @timoconnellaus)
  • time.gmtime, time.strftime and datetime.timezone build 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. Now time.gmtime() gives a time.struct_time (fields, indexing, tm_zone), time.strftime(fmt, t) formats it, and time.time_ns() is there. Datetimes can be aware: datetime.timezone.utc, datetime.UTC, timezone(timedelta(...)), tzinfo= in datetime(...), combine(d, t, tz), replace(tzinfo=...), now(tz), fromtimestamp(ts, tz), utcfromtimestamp, astimezone(tz), timestamp(), utcoffset(), tzname(), %z/%Z, offsets in isoformat(), str() and repr(), 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, the Z was dropped and the result was naive (silent wrong output). Local time (time.localtime, strftime without a tuple, a naive timestamp(), fromtimestamp without 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.py said main.py line 4: time.gmtime is not supported when 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 "" where x came from json.loads was typed as a bool, so the typed build's Rust did not compile and the file took the slower dynamic build. A JSON value or a string or int is now a JSON value. Also: re.search, re.sub, re.findall and friends on a JSON string build typed (they stopped the typed build with no method named re_find found for enum Json).
  • json.load(open(path)) builds typed (#195). It stopped check with unknown function __open_r(), which read like an internal error; it now reads the file as with open(path) as f: json.load(f) does. Other uses of an open() file the typed build cannot take (passing it to a function, for example) get a plain message instead.
  • The dynamic build's re is 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 with re.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, plus re.sub) the dynamic build went from 0.86x to about 1.8x CPython's speed.
  • Silent wrong output fixed: \N{NAME} in .py files. 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 computed CJK UNIFIED IDEOGRAPH-/HANGUL SYLLABLE one, is refused and left to CPython. (#185, thanks @timoconnellaus)
  • Silent wrong output fixed: float sum(). Typed builds added floats one by one, so sum([0.1] * 10) printed 0.9999999999999999 and sum([1e100, 1.0, -1e100]) printed 0.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, and sum([-0.0]) prints 0.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.py hid it by only comparing zero with non-zero; it now compares exact exit codes. .bv programs still exit 101 after a runtime error. (#187, thanks @timoconnellaus)
  • A .py file 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") and xs.push(1) on a list used to build and print an answer; now check and build report one error with the Python spelling and brevi run falls back to CPython, which gives the real error. A class's own push still works, and .bv files 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)), where grow changes the list (or a global, or self), printed the list from before the change in typed builds, and r = (q, q.pop()) kept a copy of q; 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, so print({3, -1, 5}) printed the set sorted. Such a file is no longer built typed: brevi run runs it with python3, and build and check report an error. Files with a bare allow line now get the guard's checks too. (#191, thanks @timoconnellaus)
  • Silent wrong output fixed: d.copy() and list(d) on a dict in typed builds. d.copy() gave the dict's keys, so print(d.copy()) showed {'a'} for {'a': 1}, and list(d) printed as a set ({'a'} for ['a']). d.copy() is now a copy of the dict (also with tuple keys) and list(d) a list of its keys. Found from the conformance run.
  • Silent wrong output fixed: comparing dict views in typed builds. d.items() <= e.items() and d.keys() == {1, 2} follow set rules in CPython; the typed build compared them as lists. Programs that compare keys(), items() or values() with <, <=, >, >=, == or != now take the dynamic build, which now compares key and item views with sets and with each other as CPython does (values() views compare by identity, so d.values() == d.values() is False). Found from the conformance run.
  • Dict subclasses in dynamic builds: __missing__ and dict(mapping). d[k] on a dict subclass with __missing__ now calls it for a missing key (a Counter-style c[k] += 1 works), and dict(sub), dict(obj) and d.update(obj) copy a dict subclass, or any object with keys() and __getitem__, as CPython does instead of gi...
Read more

Brevi v0.24.6

Choose a tag to compare

@github-actions github-actions released this 04 Nov 02:11
a8e3ca3

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, and open() 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. --sandbox still refuses these imports unless the caller grants them (see the next entry). brevi check and --native stay strict.
  • Under --sandbox only the caller grants capabilities. A # allow read, # allow write or # allow native line 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,pydantic for CPython on the named Python libraries only (not rust fn, crates, or other libraries); in the MCP tools these are the allow and allow_py arguments. 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, hashlib and base64 build natively. b"..." literals were refused by the dynamic build and hashlib/base64 ran in CPython; they now build natively (dynamic build), also under --sandbox. Bytes values have bytes(...), str.encode/bytes.decode (UTF-8, ASCII, Latin-1 with strict/replace/ignore, CPython's error messages), indexing, slicing, iteration, in, +, *, comparisons, hashing and the common methods. hashlib has MD5, SHA-1, SHA-224/256/384/512 and new (pure Rust, no crates); base64 has b64, urlsafe and b16 encoding and decoding with CPython 3.13's padding rules and binascii.Error messages. 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').
  • argparse builds 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, -h help, 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.FileType and fromfile_prefix_chars, which open files, still run in CPython. Also: brevi run x.py gives the program sys.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.
  • textwrap builds natively. wrap, fill, shorten, dedent, indent and TextWrapper (all options but fix_sentence_endings) now run in the dynamic build, also under --sandbox, breaking lines exactly where CPython does (hyphenated words, em-dashes, long words, max_lines and placeholders); 1,000 random texts wrapped at random widths print what python3 prints.
  • random builds 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,000 randint calls 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 using seed, random, randint, randrange, choice, choices, shuffle, sample, uniform, gauss, getrandbits or Random with 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 check names 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 by run --explain. The message now ends with it, as (file line N: reason), and brevi run puts 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.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.5

Choose a tag to compare

@github-actions github-actions released this 03 Nov 22:58
4f12e4f

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 == [] (and j == {}) was true for a JSON null, 0, False or "", and False == 0 on a JSON value was false; both now answer as Python does. v is True / v is False on 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 = None in a nested def with nonlocal players was not seen by the outer function's players 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 setdefault changes lost. d = labels.setdefault(s, {}), lst = d.setdefault(k, []), then changing lst changed only copies in typed builds, so every trip of a typed trains program printed "no route". The change now reaches labels.
  • 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.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.4

Choose a tag to compare

@github-actions github-actions released this 03 Nov 22:06
9003008

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, []) and j.get(k, {}) on JSON values, json.load(sys.stdin), any(f(x, seen) for ...) where f changes seen, deps or [], names set inside a loop (or just before its break) and read after it, and return None from a function returning JSON now build typed. A MISSING = 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 as None/null. Keys only some dicts have are now left out while unset, as in Python, and k in r and r.get(k) see whether the key is set.
  • csv.writer(sys.stdout) on the typed path. w = csv.writer(sys.stdout, lineterminator='\n') (also through out = sys.stdout used only for the writer) with w.writerow(row) and w.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= and lineterminator= 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.DOTALL and re.M/re.MULTILINE on the typed path. re.compile(r'\{\{(.*?)\}\}', re.S) and re.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.split skipped empty matches (re.split(r'\b', 'a b'), Python 3.7 on splits there) and gave '' instead of None for a group that took no part.
  • io.StringIO builds natively. An in-memory text file (write, getvalue, read, readline, iteration, seek, tell, truncate, print(..., file=buf), with), and so csv.writer(io.StringIO()) and csv.DictReader(io.StringIO(text)), now take the dynamic build, also under --sandbox without # allow native. The rest of io (BytesIO, open) still runs in CPython. Both saved csv_join fallback programs print what python3 prints. Also: under --sandbox the {"path":"dynamic"} line for such programs named allow native though none was needed; it now says which modules the typed build lacks.
  • bin, hex, oct and startswith/endswith with a start index on the typed path. bin(x).count('1') and tokenizers that test s.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_decode and json.decoder.scanstring build 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 and JSONDecodeError messages. These were the last 2 of the 48 final release-suite programs that still needed CPython (both json_query); both now print what python3 prints on the task's tests.
  • enum builds natively. class Color(Enum) with plain values or auto(), aliases, methods, properties and an __init__ taking tuple values, plus Color(1), Color['RED'], iteration, len, in, __members__, @unique, and str/int mixins (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, TypeError on <) print what CPython prints. IntEnum, StrEnum and Flag still 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.Fraction in 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, including x.numerator, x.denominator, Fraction("3/4"), //, %, round, abs and sum(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) with repl a 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, so m.group(2) is None stays 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 of None, and print(m.groups()) showed ['a', '', 'c'] instead of ('a', None, 'c'). A list filled with tuple(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 (printing m.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] = v on 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, \v and \a escapes, which the typed build matched as letters.
  • Path text from os and pathlib builds natively everywhere. A program whose only uses of os and pathlib are 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.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.3

Choose a tag to compare

@github-actions github-actions released this 03 Nov 19:08
f61bd0b

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 call brevi check and brevi run over 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) check and run made 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, build and run of 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, the BREVI_* 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) check of 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 (or BREVI_NO_CACHE=1) builds from scratch, BREVI_CACHE_MB caps the cache (default 1024 MB, least recently used out first; it was 400 entries), and BREVI_TIME=1 prints 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 as math.gcd sent every math. call on its line to CPython, math.sqrt included.
  • Silent wrong output fixed: re.findall with groups. With two or more groups re.findall returns 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, so len(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 from min/max of them, counts as found, so counting and grouping programs stay typed: 1 of the 765 corpus programs moved).
  • math and time on 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 constants pi, e, tau, inf and nan, and time.time, perf_counter and monotonic now build typed, whether imported with import math or from math import gcd, pi. Domain errors are a ValueError an except catches, as in CPython, and math.sqrt(-1) raises instead of giving nan. A loop of math.sqrt and math.log over 20 million numbers runs in 0.09 s (CPython: 1.9 s). Of the 47 probes, math and time programs 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.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.2

Choose a tag to compare

@github-actions github-actions released this 03 Nov 17:31
d9e2048

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, build and check print 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 (and file in a module) name the code to rewrite to get the typed path back. The typed path stays silent; --explain prints it too and adds the fix, up to five more reasons and why the dynamic build refused a CPython run; BREVI_PATH=1 prints the line for every build, BREVI_PATH=0 never. The CPython fallback's note: ran with python3; ... line is replaced by its {"path":"cpython"} line. bench/e2e.py logs each pyb task's final path and adds a typ column (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, len or set no longer counts as an order-dependent use. A sentinel MISSING = 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, {}) and d.get(k, []) on JSON objects, t[:2] and t[1:] on tuples, a lambda set inside a loop or if body, and datetime.date(...) in a function with a local named date.
  • Silent wrong output fixed: print(tuple(xs)). A tuple made with tuple(...) 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 # allow work in the sandbox. Under --sandbox and in the MCP tools (sandboxed by default), import random; random.seed(1) gave two confusing errors (unknown method .seed() and unknown 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 needs allow native. Path text no longer needs allow native either: os.path.join, basename, dirname, split, splitext, normpath, isabs, os.sep and pathlib.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, os passed 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 bare allow read still works and stays the form for .bv files), and fixes suggest the comment form.
  • Shorter fallback output. When brevi run falls 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)). --traceback or BREVI_TRACEBACK=1 shows the whole traceback; other fallbacks are unchanged.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.1

Choose a tag to compare

@github-actions github-actions released this 03 Nov 15:32
32dfbc6

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 now brevi (npx brevi mcp, npm install -g brevi), the same name as on crates.io and PyPI; it was brevi-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.py could be bypassed in three ways. A program refused for an undeclared read() 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 even sys.modules["os"] through Brevi's own sys) 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 need allow native (a Python value reaches everything CPython can, so there is no safe subset), and a program held to its allow lines (--sandbox or an allow line) 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 = 0 printed P_D { x: 1, y: 0 } instead of P(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 for repr() and !r, and a class with neither prints <__main__.C object at 0x...>. Dynamic builds also leave field(repr=False) fields out.
  • Python first everywhere. The MCP server builds code as plain Python by default (kept as main.py, built like brevi run main.py); before, it kept every program as main.bv, so plain Python failed there (type(it).__name__ gave unknown function ax_type_of(), a class method call gave unknown method .speak(), and file: "main.py" on a fresh server said "no program yet"). Brevi code is still recognised by its fn items or a .bv file name. patch replaces Python defs 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 --help lists FILE.py first.
  • Python mistakes are reported, not built. brevi check, build and run on a .py file exited 0 on mistakes such as print(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. check and build now stop with the error and a hint (name 'totl' is not defined, did you mean total?); run runs 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 a sys.version_info, sys.platform or TYPE_CHECKING test, an if False: or x 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\ude00 pairs (Python's default json.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/cancelled stops 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, timeout on run and test, brevi mcp --timeout S --mem-mb MB (Linux: a program gets at most half the machine's memory by default), run with cases falls back to CPython like run does (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 \v printed their backslash text instead of the character, and repr() 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 run with 1 and no message; it now prints one JSON line ({"e":"signal",...}) on stderr and exits 128 + the signal. BREVI_MEM_MB limits 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.py builds the pages from site/docs/*.md and the repo's own docs; bash site/deploy.sh --preview deploys to workers.dev for review.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs. On Linux and macOS, sh install.sh installs rustc if it is missing and picks the right download; on Windows, install Rust with rustup and unzip the .zip. Details: https://brevi-lang.org/docs/install

Brevi v0.24.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 13:26
8df6f2e

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 speed tasks, 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-hint leaves 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 (what brevi run uses) of a program it has not seen takes about 35% less time: the 12 speed programs in bench/compare went 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, which bench/compare uses, 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) and max of three or more values, and built-ins passed to map (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, hex and int(s, 2) / int(s, 16) on big ints are linear (a 360,000-bit bin() took 11 s); 2 ** n is 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) or out += xs on 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 >> n with 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 giving None on 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) and any(...) 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 using fractions build over dynamic values, where a Fraction takes any denominator (unit tables stopped with "can't be held exactly"). Dynamic builds add datetime.time (dt.time(), time(9, 30), datetime.combine(d, t)) and Decimal.scaleb.
  • Bench. e2e.py's pyb check also runs CPython on the same files and logs the pair per check (shadow); the summary's bug column counts checks that failed only because of Brevi. New set speed (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).

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs; sh install.sh fetches it and picks the right download.

Brevi v0.23.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 09:37
2b5dc10

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 --standalone puts 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 loop s += x % 10; x //= 10 over a 40-digit number, and -h // 7. Top-level int constants such as MOD = 1000000007 join 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 .bv or .py file 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.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rustc on the PATH to build programs; sh install.sh fetches it and picks the right download.

Brevi v0.22.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 06:11
9408924

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; any xs[a:b:k] now picks as Python does, also on text); zs = xs followed by xs.sort(), xs.extend(...), xs += [...] or del xs[a:b] left zs unchanged (now built over dynamic values, where both names share the list); format specs with a fill character or the b, o, g, e, #, _, = and nested {x:{w}.2f} forms ('{:-^5}'.format('a') printed a); 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 printed 1000.0 as 1000. 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.center padded 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 - (now x-y-z). -0.0 % 5.0 printed -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.rpartition ran as s.partition); Python files are never auto-fixed now. Printing a re match showed its groups (['12'], []) instead of <re.Match object; ...> or None (such files now build over dynamic values). Speed: in dynamic builds every s.add(x), len(s) and s |= t copied 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, OverflowError or RecursionError build 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 ** 5000 printed inf (OverflowError), 0.0 ** -1 and (-8) ** 0.5 printed 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") and float("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), and sys.setrecursionlimit still allows deep recursion.

  • Fast sets in typed builds. Typed builds kept a Python set as a plain list, so every x in s scanned 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 with in and otherwise only added to, popped, counted, sorted or walked (a function's own seen, or a parameter used that way) is now kept with a hash index, so in takes 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 is deque.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), and while 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 with pop(0)) that is otherwise only added to, popped, read by index or walked is kept as a ring buffer, so popleft() 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(), then x 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 as return n * fact(n - 1) or fib(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.

  • Still open: a growing value captured by a nested function (a closure or nonlocal) stops with an overflow error.

  • Fast by default, and the Codon/Nuitka gaps closed. brevi run and brevi build without --release now compile the program at opt-level 1 against a fully optimised runtime, so the default build beats CPython on all 12 speed programs in bench/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-character split and {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 factorial f *= i, a rolling hash h = h * 31 + ord(c), Fibonacci a, b = b, a + b, big literals) become big ints, 3 ** 200 % m is 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 from with 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.DictReader is native (rows are dicts of str, so d[r["dept"]] no longer guesses int keys), __file__ is the file's absolute path, and the sys.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 --release runs 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 bids followed by book.remove(o) changed a copy (such files now build over dynamic values); m.group(3) is None was 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, and 10 ** -2 stopped with an error instead of giving 0.01.

Prebuilt brevi compiler for Linux, macOS and Windows. brevi still needs rus...

Read more