DEV Community

Cover image for I built a programming language because C++ almost killed my drone
subhansh
subhansh

Posted on

I built a programming language because C++ almost killed my drone

I'm 17. Last year I was writing drone firmware in C++ and Python. One afternoon my ultrasonic sensor returned null during a landing sequence. The code didn't check for it. The drone tried to divide by zero, the motor controller panicked, and my DJI Tello-sized quadcopter dropped from 3 meters onto concrete.

That was a $400 lesson in why runtime errors in robotics aren't just bugs. They're physics problems.

So I built a programming language.


The problem nobody talks about

When you're writing a web app and something crashes, you reload the page. When you're writing drone code and something crashes, the drone crashes. Into whatever's below it. Usually something expensive or fragile or alive.

C++ and Python dominate robotics. Both assume you'll handle edge cases yourself. Sensor returns null? That's your problem. Timing deadline missed? Good luck. No fallback path for when the lidar fails over water? Figure it out.

ROS (Robot Operating System) helps with some of this, but it's an entire infrastructure layer. What I wanted was something simpler: a language where the compiler catches the stuff that kills robots before you ever compile it.

Here's what I mean. In C++, you write:

float distance = sensor.read();
float speed = target_distance / distance;
motor.set(speed);
Enter fullscreen mode Exit fullscreen mode

If distance is 0, you get a division by zero. If sensor.read() returns NaN, you get NaN propagation through your motor controller. The compiler doesn't care. It'll happily compile this and let your drone fall out of the sky.

In Fabric (the language I built), you'd write:

sensor distance: Sensor<f32, ±0.02>
actuator motor: Motor

loop {
    let speed = target_distance / distance
    motor.write(speed)
}
Enter fullscreen mode Exit fullscreen mode

If you try to use distance without checking that the sensor returned a valid reading, the compiler throws an error. If distance could be zero and you didn't add a fallback path, the compiler throws an error. If your sensor has an uncertainty bound of ±0.02 and you're doing math that could amplify that uncertainty past a safety threshold, the compiler throws an error.

The compiler knows about physics. Not perfectly, but enough to catch the stuff that actually kills robots.


How it works under the hood

I wrote the whole thing in Rust. About 4,500 lines across 8 crates. Here's the pipeline:

.fab source → Lexer → Parser → AST → Type Checker → Fallback Graph → IPET Timing → CodeGen → .py / .c
Enter fullscreen mode Exit fullscreen mode

Each step catches a different category of mistakes.

The type system tracks sensor uncertainty as a first-class concept. When you declare Sensor<f32, ±0.02>, that ±0.02 isn't decoration. It propagates through every math operation. Add two sensors with ±0.02 uncertainty and the result has ±0.04. Multiply by a constant and the uncertainty scales accordingly. If the final result's uncertainty exceeds what's safe for your actuator, the compiler catches it.

The fallback graph checks that every sensor has a plan B. Not just declared, but actually reachable. If your fallback function A calls fallback function B, and B calls A, that's a cycle. The compiler detects it. If sensor X has no fallback at all, the compiler refuses to build.

IPET timing analysis uses an Integer Linear Program to prove worst-case execution time at compile time. It builds a control flow graph, assigns cycle costs to each instruction (ARM Cortex-M4 model: ALU = 1 cycle, float division = 14 cycles, sensor read = 2 cycles), and solves for the maximum possible execution time across all code paths including loops.

I didn't come up with this technique. It's from Li & Malik's 1994 paper on implicit path enumeration. But I implemented it in Rust using a pure-Rust ILP solver called good_lp with the microlp backend. No external solver dependency. No C FFI. Just Rust solving linear programs.

The solver maximizes sum(cost_i * x_i) subject to flow conservation constraints. If the solver fails (which happens on really complex control flow), the system falls back to a conservative estimate instead of crashing.


The two-target code generation

Fabric compiles to two backends: Python for Webots simulation, and C for ARM Cortex-M hardware.

Python output generates a Webots controller class with sensor initialization, motor setup, fallback state tracking, and all the logic. You can test your drone code in simulation before touching hardware.

C output generates code that includes a hal.h hardware abstraction layer. Static sensor handles, #define TIMEOUT_MS for fallback deadlines, proper C types (float, int), and deadline enforcement using hal_get_time_ms().

Same source file, two completely different targets. No #ifdef in your code. No conditional compilation. The compiler handles the target-specific stuff.


What I'd do differently

The parser is hand-rolled. About 700 lines of Pratt parser with recursive descent for statements. I started with chumsky (a Rust parser combinator library) but their API changed between versions and the documentation was sparse when I needed it most. So I wrote my own.

It's fine. But it means every time I want to add a new syntax construct, I'm writing parser code by hand. A proper parser generator would've saved time in the long run, even with the initial investment.

The loop bound estimation is a heuristic. If your loop body has 1-3 statements, the compiler assumes it runs 10 times. 4-6 statements? 5 times. 7+? 3 times. This is obviously wrong for real programs. Proper static bound analysis would make the IPET results much more accurate. It's on my list.

The C output references a clamp() function that it never defines. If you actually try to compile the generated C code on hardware, you'll get a linker error. I know about it. I just haven't fixed it yet.


Why I'm publishing this

I put all 8 crates on crates.io. The lexer, parser, type checker, everything. The repo is at github.com/subhansh-dev/fabric.

Partly because I want other people to try it. If you're working on robotics or drones and you've been bitten by runtime errors, Fabric might save you some pain.

Mostly because I think the idea matters more than my implementation. The safety-critical systems world has had formal methods and static analysis for decades. But it's all locked behind expensive tools and enterprise licenses. I wanted to build something that a 17-year-old with a Raspberry Pi could use.

Is my implementation perfect? No. The IPET loop bounds are guesses. The C backend doesn't handle drone swarms. The match expression codegen references some variables it doesn't always generate. I know about all of these. They're bugs, not features, and I'll fix them.

But the core idea is sound: compile-time safety for robotics shouldn't require a $50,000 license for Simulink. Sometimes the right answer to "how do we prevent this bug" isn't "write better tests" or "add more runtime checks." Sometimes it's "make the compiler say no."


What's next

I'm working on adding hardware-in-the-loop testing support. The idea is that Fabric could instrument your actual hardware and compare runtime sensor readings against the compile-time uncertainty bounds. If reality diverges from what the compiler predicted, you get a warning before your next flight.

Also working on proper static loop bound analysis. The current heuristic is fine for demos but useless for real deployments. I'm looking at abstract interpretation techniques to actually prove bounds instead of guessing.

If any of this sounds interesting, check out the repo. Open an issue. Try it on your own robot. Break it and tell me how.

That's how this gets better.


Fabric is MIT licensed. The crates are published on crates.io. The docs are in the README because I'm 17 and haven't written proper docs yet.

gh repo link :- https://github.com/subhansh-dev/fabric

Top comments (0)