Breviv0.24

How it works

Architecture

This page is built from docs/ARCHITECTURE.md in the repository, so it always describes the code that shipped.

View as Markdown · source

This document explains what Brevi does between a source file and a native program: which parts of src/ run, in what order, and why. It describes main as of v0.24.

Brevi reads two kinds of input:

Both end the same way: Brevi writes Rust source, and rustc turns it into a native program. Python files have more steps, because a Python file must give exactly CPython's output or not be built natively at all.

What runs natively#

Every diagram below colours its steps the same way:

Diagram: What runs natively
Click the diagram to enlarge it
PathNative?What runs
Typed build (every .bv, most .py)Brevi nativePlain Rust with fixed types. The fastest path.
Dynamic build (src/dynpy)Brevi nativeRust over a dynamic value type. No CPython anywhere. Slower than typed, usually faster than CPython.
Library calls (pybridge)Partly nativeThe program is native; each call into numpy, pandas, etc. runs in CPython loaded into the program.
CPython fallback (brevi run only)Not nativeThe whole file runs on python3, with a one-line path report on stderr.

The release test counts how many final programs build natively (typed or dynamic, no fallback); the bar is 90%.

Which path ran#

brevi run, brevi build and brevi check say which of these paths a Python file took, as one JSON line on stderr, so an agent can tell when it is off the fast path and which function to rewrite:

{"path":"dynamic","line":12,"fn":"parse","why":"rows holds [int] (set on line 9) but is set to [str] here"}
{"path":"pybridge","why":"numpy run in CPython","py":["numpy"]}
{"path":"cpython","line":2,"fn":"g","why":"Brevi could not build it: unknown function eval()","by":"python3"}
pathMeaning
typedTyped build: the fast path. Not printed unless --explain or BREVI_PATH=1.
pybridgeTyped build; py lists the modules (and functions of Brevi's own modules it lacks, like math.gcd) that run in CPython.
dynamicDynamic build. why is the typed build's first refusal (or the guard's reason), line and fn where it is, file when it is in an imported module. py, when present, lists the libraries (and open) that CPython runs inside the program (see Library calls).
cpythonbrevi run handed the file to python3 (by). why is Brevi's first build error.

With --explain the line also carries the error's fix, up to five more typed errors (also), and for cpython why the dynamic build refused the file (dynamic). BREVI_PATH=1 prints the line for every path, BREVI_PATH=0 never. A failed build prints only its errors. The MCP server always sets BREVI_PATH=1 and returns the report as structuredContent (see mcp.rs). The code is say_path in main.rs.

The big picture#

Diagram: The big picture
Click the diagram to enlarge it

In words, for a Python file:

  1. Guard. typed_guard.rs reads the file (and the local modules it imports) and looks for code the typed build would answer differently from CPython, such as printing a set. If it finds any, the typed build is skipped.
  2. Typed build (Brevi native). The normal path: infer types, check, and emit plain Rust with fixed types. This is where the big speedups come from. Brevi's own spellings are mistakes in a .py file, so pydiag.rs stops on them before and after this build: Brevi syntax (&&, x => x + 1, fn f(x):), Brevi names (true, fail("x")), and a method only Brevi has, called on a value the typed build knows is a list, dict, text or number (xs.push(1); a class's own push is fine).
  3. Dynamic build (Brevi native). If the typed build fails at any stage (its own checks or rustc), pydiag.rs first looks at the typed errors for sure mistakes: a name that is never defined, a method no Python type has, or text added to a number. Code that may never run (under sys.version_info, sys.platform, TYPE_CHECKING or if False, and except blocks) is skipped. check and build report these as errors instead of building; brevi run falls back to python3 and names the mistake in its path report. Otherwise the file is rebuilt over a dynamic value type that behaves like CPython's objects. Still native, no CPython involved, with one {"path":"dynamic"} line saying why (see Which path ran).
  4. CPython fallback (not native). Only if the dynamic build refuses too. brevi run x.py then runs the file with python3 and prints one {"path":"cpython"} line on stderr. brevi build, brevi check and --native (or BREVI_NATIVE=1) never fall back; they print the errors.

A Brevi file only takes the typed path. Its errors go straight back to the model as JSON.

Commands#

All commands live in src/main.rs.

CommandWhat it does
brevi run FILEBuild, then run. For .py, falls back to python3 if no native build works.
brevi build FILE [-o OUT]Build a native program.
brevi check FILE [--fast]Front-end checks, then rustc --emit=metadata (no binary). --fast skips rustc. Replayed from the result cache when the files did not change.
brevi test FILEBuild with --test and run the program's test blocks.
brevi emit FILEPrint the generated Rust.
brevi fix FILEApply every suggested fix to the file.
brevi patch FILE PATCHReplace whole functions/types by name (src/patch.rs).
brevi guide [SECTION]Print the language guide or one section.
brevi mcpMCP server over stdin/stdout (src/mcp.rs): guide, check, run, test, patch, edit, show.

Flags: --release (opt-level 3), --sandbox (only what the caller grants with --allow read,write,native / --allow-py MODULES; the file's allow lines grant nothing), --window (real tkinter windows via egui), --explain, --native.

Stage 1: front end#

These steps run on the source text before parsing.

StepFileWhat it does
Join filesmodules.rsuse "x.bv", and import x of a sibling .py file or package, are joined into one program. A segment table maps every line back to its file, so errors name the right file.
Capabilitiescaps.rsUnder --sandbox, only what the caller grants (--allow read,write,native, --allow-py MODULES, the MCP allow / allow_py arguments) counts, and an allow line in the file that asks for more is refused. Without it, a program with allow read, write lines is held to them. A program that uses something not granted is refused, before any other path runs: a refused program is never built over dynamic values, through pybridge or run with python3. Python libraries (pybridge) need allow native, and a sandboxed program never falls back to python3. In a .py file the line may be a comment (# allow read). Path text (os.path functions, the os path constants and pathlib.PurePath) is built from Brevi's own Python copies (src/dynpy/py, over dynamic values, no file access) with or without the sandbox, so a program that only handles path text stays native; without allow native, random uses Brevi's copy too, and caps::path_uses refuses every other use of os and pathlib by name.
Foreign codeffi.rsuse CRATE "VER", rust fn, py fn, rust: and py: blocks. Foreign bodies are cut out and replaced by a stub on the same line, so line numbers stay put. Any crate switches the build to cargo.
Autofixmain.rs autofixFor .bv only: errors with a sure one-word fix (a typo of a known name) are fixed before the build and reported on stderr. Python files are never rewritten, because CPython would not rename s.rpartition either.

A file counts as Python if it ends in .py, or if it uses spellings only Python code has (def, class, import, None, .append(, ...). See python_style in main.rs. Python mode turns on the Python-only passes below.

Stage 2: the typed build#

Brevi native. Everything this stage produces is compiled Rust. Only values from bridged libraries (below) are handed to CPython.

Diagram: Stage 2: the typed build
Click the diagram to enlarge it

The driver is compile_once in main.rs. In order:

  1. Lex and parse (lexer.rs, parser.rs, ast.rs). One parser reads both Brevi and Python spellings into one syntax tree. Python forms are rewritten into Brevi ones as they are parsed: nested defs are lifted, len/sum/append become Brevi methods, classes become types with methods. A syntax error is a JSON line with the line number and, where it can, a fix.
  2. Parser hooks. gui.rs takes calls on tkinter widgets. pybridge.rs marks every name that holds a value from a Python library (see Library calls). Because a name can be used before the line that assigns it, the parser runs again while new names are found.
  3. Big integers (bigint.rs, Python mode). Python ints have no size limit. A number read from text that is only added, multiplied, compared and printed becomes a big integer. Powers of literals are worked out at compile time, so 3 ** 50 % 1000000007 gives Python's answer.
  4. Sharing (alias.rs, Python mode). In Python, r = rows[k]; r[0] -= 1 changes rows. Brevi values are copies, so after each statement that changes r in place, the copy is written back (rows[k] = r). Where a change would still be lost, the build is refused instead of giving the wrong output.
  5. Records (records.rs, Python mode). A dict with fixed string keys and values of different types ({"n": n, "date": d, "posts": []}) becomes a record type, like a class. It still prints as Python prints the dict and still serialises with json.dumps.
  6. Inference and checks (infer.rs). Works out the types Brevi programs leave unstated: the item type of xs = [] from later uses, return types, which parameters a function changes (auto_mut), and which method a call on an object means. Then infer::check runs Brevi's own type checker, with short errors such as "str + int" or "type changes from int to str", each with a fix.
  7. Error passing (emit.rs auto_try). A call to a function that can fail passes its error up unless the code already handles it, so programs need no ?.
  8. Ownership (moves.rs). A variable's last read moves the value instead of copying it. If moving breaks Rust's borrow rules, the build retries without moves (see Stage 4).
  9. Emit (emit.rs). Writes one Rust file: the runtime prelude, then the program. Each statement records its source line (ax_l(n)), so a runtime error can name the Brevi or Python line.

The typed build may also ask pybridge::learn to bridge a missing library function (for example math.gcd) and build again, up to 8 rounds (compile_to_rust).

What "Python behaviour" means in the typed build#

The typed build copies the CPython behaviour that shows in a program's output: integer size (bigint.rs), shared objects (alias.rs), dict printing and JSON (records.rs), set membership with a hash index (emit::FAST_SETS), Python's float and round formatting, exception messages (ax_exc_msg in prelude.rs), and so on. Anything it cannot match is not built typed. Either the typed build refuses it (and the dynamic build gets a turn), or typed_guard.rs sends it to the dynamic build before the typed build starts.

The guard: when typed would be wrong#

Some Python programs type cleanly but would still print something different from CPython. typed_guard.rs finds them on the Python syntax tree (it reuses the dynamic build's parser) and sends them straight to the dynamic build. Current cases:

A file the guard cannot read (its parser rejects it) has passed none of these checks, so the typed build may not build it either: if the typed front end accepts it, it is refused with the guard's reason. A file the typed build refuses too keeps the typed errors. Either way brevi run falls back to CPython.

BREVI_GUARD=print brevi build x.py prints the guard's verdict.

Before the guard and the typed build, pysentinel.rs rewrites a sentinel MISSING = object() to None when the program cannot tell the two apart (see the rules at the top of that file).

Stage 3: the dynamic build (src/dynpy)#

Brevi native. No CPython is involved: the program and its runtime are compiled Rust.

For Python files the typed build cannot handle, such as a variable that holds an int and later a string, or a list of mixed values.

Diagram: Stage 3: the dynamic build (src/dynpy)
Click the diagram to enlarge it

Typed builds are faster. The dynamic build was about 1.3x CPython on micro benchmarks and 4.5x on the minilang tests when it landed.

Switches: BREVI_DYN=1 forces the dynamic build, BREVI_DYN=0 disables it, BREVI_DYN_DEBUG=1 prints rustc errors from generated code.

Library calls (pybridge)#

Partly native. The program is native; only the library calls themselves run in CPython.

For Python files that import libraries Brevi does not build itself (numpy, pandas, requests, sqlite3, argparse, ...).

Diagram: Library calls (pybridge)
Click the diagram to enlarge it

Stage 4: compiling the Rust#

Brevi native. Typed and dynamic builds both end here, as a native executable.

Diagram: Stage 4: compiling the Rust
Click the diagram to enlarge it

Stage 5: running#

Brevi native, unless the CPython fallback below is used, which is not native.

brevi run runs the built program with the arguments after -- and exits with its status. A runtime error prints one JSON line: {"e":"runtime","line":N,"msg":"..."}, with the message in Python's words for Python files.

For brevi run x.py without --native, Brevi runs a strict build of itself as a child process. If that fails, it runs python3 x.py (or $BREVI_PYTHON) and prints a {"path":"cpython",...} line on stderr naming the line, function and error that stopped Brevi, so an agent always gets a result and knows it ran on CPython. If there is no Python either, it prints Brevi's errors.

Other pieces#

FileRole
gui.rs, gui_glue.rs, runtime/brevi_tktkinter programs compiled onto the brevi_tk runtime. Headless by default (prints the window as text, reads actions from stdin); --window draws real windows with egui.
diag.rsThe JSON error format: {"line":..,"msg":..,"fix":..}, plus machine-applicable edits used by autofix and brevi fix.
patch.rsbrevi patch: edit by whole items, so a retry costs only the change.
mcp.rsbrevi mcp: keeps the program in the server so tools can send only changes.
json.rsMinimal JSON reader for rustc's error output.
grammar/A Lark grammar of Brevi for constrained decoding; CI parses every test program with it.

Where to look when#

QuestionStart in
Why did my Python file build dynamically?BREVI_GUARD=print, then BREVI_DYN=0 brevi build x.py to see the typed errors
What Rust did Brevi write?brevi emit x.py (typed) or BREVI_DYN=1 brevi emit x.py (dynamic)
Why did brevi run use CPython?The {"path":"cpython"} line on stderr names the first error and its function; brevi run --native x.py shows them all
A wrong answer compared with CPythontools/parity_probe.py; typed fixes go in infer.rs/emit.rs/prelude.rs, dynamic ones in dynpy/rt_*.rs
A new Python librarypybridge.rs (NATIVE list decides what stays native)

Tests#