Skip to main content

Python as an elegant coding bridge to bare-metal microcontroller C. A hybrid optimizing transpiler with source-mapped debugging.

Project description

barepy

barepy

Python as an elegant coding bridge to bare-metal microcontroller C.
A hybrid optimizing transpiler with source-mapped debugging.

version python targets output source-mapped status


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-table shows 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.static helpers 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.dynamic functions 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 (arena for non-escaping, gc for escaping/cyclic), surfaced in optimization-table. Opt into the cycle-collecting heap with bp.init(..., gc="mark-sweep"): allocations move onto a precise mark-sweep collector, generated code roots its live locals, and bp.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, PLL m/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 switches SYSCLK over.
  • GPIO clocks — only the ports your pins actually use are enabled, via an RCC AHB1ENR mask computed at build time.
  • SysTick timebasebp.delay(ms) waits on a real millisecond SysTick counter (WFI between 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.static helpers. 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 (generated barepy_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

barepy-0.1.0.tar.gz (79.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

barepy-0.1.0-py3-none-any.whl (76.7 kB view details)

Uploaded Python 3

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

Hashes for barepy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d47195ae5e1c861134dd942caa1e21d4f4b26b8a479aa1c6d559f9c3e2b3754f
MD5 53ecb419e379e8810c3ba0b31e3e13f4
BLAKE2b-256 7b74de190a9309579919829d9a6ea4144079d2d0b5652048b2113a68eb0cad7e

See more details on using hashes here.

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

Hashes for barepy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 71bd989e8f91b5b956bc33fcaa79ffe61884243398f33dbd0db8f1da999b516b
MD5 959c5a3a39d7b5eef1c4d796cabe28b5
BLAKE2b-256 cae23456fd7f5a850b970f8ed40360f034cc4c946b7466049f771e62b2a7c043

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page