Python as an elegant coding bridge to bare-metal microcontroller C. A hybrid optimizing transpiler with source-mapped debugging.
Project description
barepy
Python as an elegant coding bridge to bare-metal microcontroller C.
A hybrid optimizing transpiler with source-mapped debugging.
Write firmware in a clean Python subset. barepy transpiles it to readable C, compiles it for your board (or a native simulator), and — crucially — maps every compiler and runtime error back to the exact Python line and column.
import barepy as bp
from barepy.boards import STM32F4
bp.init(STM32F4, clock="168MHz")
led = bp.Pin(13, bp.OUTPUT)
btn = bp.Pin(0, bp.INPUT_PULLUP)
@bp.setup
def setup():
led.off()
@bp.loop
def loop():
led.toggle() if btn.pressed() else led.off()
bp.delay(200)
barepy sim examples/blink.py # transpile -> C -> compile -> simulate
✨ Highlights
| 🐍 Python in, C out | A static, zero-overhead subset transpiles to small, fast, readable C — not an on-chip interpreter. |
| 🎯 Source-mapped everything | Compiler, board, and runtime errors surface against your Python with a caret — never the generated C. |
| ⚖️ Hybrid tiers | Most code is zero-cost static. Genuinely dynamic code opts into a small boxed-value runtime via @bp.dynamic. Pay only for what you use. |
| 🔬 Real optimizer | Fix-point const-fold/prop, algebraic identities, branch-fold, DCE — every rewrite auditable with optimization-table. |
| 🛡️ Panic-free | Every pipeline stage runs inside a fault barrier; a 1,500-case fuzzer asserts zero escapes. Runtime faults degrade gracefully. |
| 🔥 Hot-code analysis | Ranks patterns dangerous on an MCU: blocking in an ISR, while True with no exit, div-by-zero, 32-bit overflow, busy loops. |
| 🔌 Everything is a plugin | Boards, backends, passes, tiers, CLI/TUI commands, lints, flashers — all extension points on a plugin fabric. |
| 🖥️ Textual studio | Cockpit TUI around your IDE: diagnostics (incl. C-level errors mapped to Python) · cost · optimizations · hot-code · generated C · sim · debug-C memory profiler · flash · test suite · live activity monitor. |
Why it exists
A transpiler, not an on-chip interpreter — the output is native C, so it is
fast and small. It is hybrid: most code is a static, zero-overhead subset;
genuinely dynamic code (config parsing, protocol work) can opt into a small
boxed-value runtime with @bp.dynamic. You pay only for what you use, and the
build report tells you the cost.
Install
pip install barepy # core (from PyPI)
pip install 'barepy[studio]' # + Textual TUI
# from source
pip install -e .
pip install -e '.[studio]'
Requires Python 3.11+ and a C compiler (cc for the native sim,
arm-none-eabi-gcc for ARM builds).
Commands
| command | does |
|---|---|
barepy transpile f.py [--show] [--no-opt] |
emit C + .map.json, no compile |
barepy build f.py [--target sim|arm] |
transpile + optimize + compile |
barepy sim f.py [--ticks N] |
build native + run the simulator |
barepy debug f.py |
build; render any error with a caret on Python source |
barepy optimization-table f.py |
table of every optimization applied |
barepy hot-code f.py [--strict] |
ranked report of fragile / dangerous code |
barepy board list / board show <name|file> |
inspect boards |
barepy plugins list |
show discovered plugins + extension points |
barepy studio [f.py] |
launch the Textual TUI |
The compiler
The pipeline is a real optimizing compiler, not a translator:
-
Type inference (M1) — flow-forward inference over the static tier assigns a concrete C type to every local (
int32_t x,bool, …) and reports type errors against Python spans. -
Optimizer (M2) — fix-point passes: constant folding, constant propagation, algebraic identities (
x*1,x+0,x*0), branch folding (if True), and dead-code elimination.barepy optimization-tableshows each rewrite:line pass kind before -> after 14 const_fold arith (2 + 12) -> 14 15 algebraic x*1 (x * 1) -> x 17 branch_fold if-true if <true> -> <then branch> 19 const_fold arith (14 + 100) -> 114 -
SSA layer (M2+): inlining · monomorphization · copy-prop — a whole-program pass inlines single-return
@bp.statichelpers at their call sites (binding parameters to the actual argument trees), monomorphizes un-annotated helpers per call-site type signature, and propagates single-definition (trivially SSA) copies into their uses. A fully-inlined helper emits no C function at all — it costs nothing:line pass kind before -> after 24 monomorphize specialize dbl(...) -> dbl<int> 24 inline expand dbl(...) -> ((21 * 2) + 1) 25 monomorphize specialize dbl(...) -> dbl<bool> 26 copy_prop propagate a -> ((21 * 2) + 1) 26 const_fold arith (42 + 1) -> 43 -
Hybrid dynamic tier (M4) —
@bp.dynamicfunctions transpile to C over a boxed-value runtime (Obj*, bump arena). dict / list /for/ unpack /int()/.split()/.get()all work. The static caller unboxes scalar results and resets the arena at the boundary — you pay only for what you use. Generated dynamic C is portable C11 — container literals are hoisted into ordinary statements, so nothing depends on GCC/Clang statement-expressions. -
Escape analysis → region inference → GC (M4+) — escape analysis classifies every dynamic allocation into a region (
arenafor non-escaping,gcfor escaping/cyclic), surfaced inoptimization-table. Opt into the cycle-collecting heap withbp.init(..., gc="mark-sweep"): allocations move onto a precise mark-sweep collector, generated code roots its live locals, andbp.gc_collect()reclaims unreachable objects — including reference cycles the bump arena never could:@bp.dynamic def churn(n: int) -> int: i = 0 while i < n: d = {} d[b"self"] = d # a reference cycle bp.gc_collect() # reclaimed each iteration; live memory stays flat i = i + 1 return bp.gc_live() # bounded, no matter how large n is
Hot-code analysis
barepy hot-code ranks patterns that are dangerous on a microcontroller:
blocking or allocating in an ISR, while True with no exit, division by zero,
32-bit overflow, ValueError-prone unpacking, busy loops, and more.
🟥 CRIT scary.py:22 [isr-blocking] in on_button()
blocking bp.delay() inside ISR 'on_button' stalls the whole system
fix: ISRs must return fast; set a flag instead
Fault tolerance (aerospace-grade)
The transpiler is panic-free: every pipeline stage runs inside a fault
barrier, so any internal bug becomes a contained ICE diagnostic, never a crash.
A 1,500-case fuzzer of malformed / adversarial / binary input asserts zero
escapes (tests/test_robustness.py). At runtime, dynamic-tier faults
(bad int(), unpack mismatch, KeyError) print a source-mapped error and the
program degrades gracefully instead of hard-faulting.
Source-mapped errors
barepy emits #line directives into the generated C and keeps a span sidecar,
so a mistake surfaces against your Python, not the generated C:
$ barepy debug examples/bad_blink.py
examples/bad_blink.py:18:5 [static]
btn.toggle()
^^^^^^^^^^^^
error: 'btn.toggle()' needs an OUTPUT pin, but 'btn' is INPUT_PULLUP
hint: declare btn = bp.Pin(0, bp.OUTPUT)
Board-level constraints map back too:
$ barepy build examples/blink.py --board examples/custom_board.board.toml
examples/blink.py:4:1 [board]
bp.init(STM32F4, clock="168MHz")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: clock 168MHz exceeds MyTinyBoard max 48MHz
Define your own board
Three ways, all producing the same Board object.
1. Data file (*.board.toml) — shareable, no code:
name = "MyTinyBoard"
[mcu]
core = "cortex-m0"
flash = { base = 0x08000000, size = "64KB" }
ram = { base = 0x20000000, size = "8KB" }
[clock]
max_hz = 48_000_000
[peripherals]
GPIOA = 0x48000000
[[pins]]
id = 13
port = "GPIOA"
bit = 5
barepy build blink.py --board my.board.toml
2. Python class — full control (see barepy/boards/__init__.py).
3. Plugin package — ship a board (and flasher, backend, passes…) on PyPI.
Hardware bring-up (real clock / RCC / SysTick)
The ARM target no longer assumes a hand-configured clock. Each build emits a
barepy_board.h derived from the Board definition, and the runtime runs the
real bring-up sequence:
- PLL / RCC — from the board clock tree (
HSE, PLLm/n/p) barepy computes the core frequency (e.g. STM32F4:8 MHz × 336 / (8 × 2) = 168 MHz), sets the flash wait states, programs the PLL, and switchesSYSCLKover. - GPIO clocks — only the ports your pins actually use are enabled, via an
RCC AHB1ENRmask computed at build time. - SysTick timebase —
bp.delay(ms)waits on a real millisecond SysTick counter (WFIbetween ticks), not a tuned busy-loop.
// generated barepy_board.h (STM32F4 @ clock="168MHz")
#define BP_CORE_HZ 168000000u
#define BP_RCC_BASE 0x40023800u
#define BP_FLASH_WS 5u
#define BP_GPIO_ENR_MASK 0x00000005u // GPIOA + GPIOC (the pins in use)
#define BP_HAS_PLL 1
#define BP_PLL_M 8u #define BP_PLL_N 336u #define BP_PLL_P 2u
Plugins — everything is an extension point
A plugin is a package exposing a Plugin through the barepy.plugins
entry-point group. It can register boards, backends, MIR passes, runtime tiers,
CLI/TUI commands, lints, and flashers. See examples/plugins/barepy_tinyavr/.
from barepy.ext import Plugin, hookimpl
class plugin(Plugin):
@hookimpl
def register_boards(self, reg): reg.add(MyBoard)
@hookimpl
def register_flasher(self, reg): reg.add("avr", avrdude_flash)
Architecture
Python source
-> frontend tokenize/AST -> IR (+ spans) barepy/frontend.py
-> analysis effect/tier/pin checks barepy/frontend.py:analyze
-> board resolve + validate barepy/hal.py
-> codegen IR -> C + #line + source map barepy/codegen.py
-> driver write artifacts, compile barepy/driver.py
-> runtime T0 static core (native | arm) barepy/runtime/
The IR carries a Span on every node — the source-map spine that lets any
failure at any layer point back to Python. Later milestones lower the IR
through SSA/MIR (optimizer) and LIR (boxing, coroutines, ISR lowering) without
changing this pipeline shape.
Known limitations
barepy 0.1.0 is the M0 vertical slice — honest about its edges:
- Inlining is limited to single-return
@bp.statichelpers. General multi-statement static functions with parameters are inlined only in that expression form; a full MIR call-lowering is future work. - The GC is a stop-the-world mark-sweep with a fixed object pool. It handles cycles correctly, but there is no generational / incremental collection yet, and collection runs only at safe points (never mid-expression).
- PLL / RCC bring-up targets the STM32F4 register layout. Other families
compile against the same generated
barepy_board.h, but non-F4 clock trees may need a board-specific recipe (ship one via a plugin backend). - Supported Python is a subset. Classes, generators, exceptions, and arbitrary imports are out of scope by design — see the frontend for the exact grammar.
Status & roadmap
- frontend → IR with spans (for, bool ops, bytes, dict/list, subscript, unpack)
- static tier → C,
#line+.map.json - effect/pin/board diagnostics with caret rendering
- native simulator + STM32 ARM runtime
- board system: Python class, TOML, validation
- plugin fabric (boards/backends/passes/tiers/commands/lints/flashers)
- Textual studio (diagnostics · C · cost · optimizations · hot-code · sim · debug-C memory profiler · flash · test suite · live activity monitor)
- M1 type inference (flow-forward, C-type assignment, type errors)
- M2 optimizer (const-fold/prop, algebraic, branch-fold, DCE) +
optimization-table - M4 hybrid dynamic tier (boxed
Obj*runtime, bump arena, boundary marshalling) - hot-code risk analysis (
hot-code) - aerospace-grade fault barrier (panic-free, fuzz-verified) + mapped runtime faults
- M2+ SSA layer: inlining, monomorphization, copy-propagation
- M4+ escape analysis → region inference → cycle-collecting mark-sweep GC
- portable dynamic codegen (C11, no statement-expressions)
- real ARM clock/RCC bring-up + SysTick
bp.delay(generatedbarepy_board.h) - M2++ full MIR call-lowering (general static functions), CSE/GVN
- M4++ generational / incremental GC
- M5 async state machines · M6 DMA ownership · M8 LLVM backend (RISC-V/ESP)
M0 is the working vertical slice this repo ships. Each later milestone is
independently useful.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file barepy-0.1.0.tar.gz.
File metadata
- Download URL: barepy-0.1.0.tar.gz
- Upload date:
- Size: 79.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d47195ae5e1c861134dd942caa1e21d4f4b26b8a479aa1c6d559f9c3e2b3754f
|
|
| MD5 |
53ecb419e379e8810c3ba0b31e3e13f4
|
|
| BLAKE2b-256 |
7b74de190a9309579919829d9a6ea4144079d2d0b5652048b2113a68eb0cad7e
|
File details
Details for the file barepy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: barepy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 76.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71bd989e8f91b5b956bc33fcaa79ffe61884243398f33dbd0db8f1da999b516b
|
|
| MD5 |
959c5a3a39d7b5eef1c4d796cabe28b5
|
|
| BLAKE2b-256 |
cae23456fd7f5a850b970f8ed40360f034cc4c946b7466049f771e62b2a7c043
|