A compiled language for robots and drones that catches your mistakes before they crash your hardware.
Real-time deadlines, sensor uncertainty, and failure handling as compiler-checked language features β not runtime hope.
I got tired of reading robotics code where the stabilization loop probably runs fast enough, the sensor readings probably aren't too noisy, and the fallback logic probably covers every failure case. "Probably" doesn't cut it when there's a drone in the air.
Most robotics software is written in C++ or Python on top of ROS. The language doesn't know or care that your IMU drops out sometimes, that your loop needs to finish in 2ms or the quadcopter flips, or that you forgot to handle the case where both GPS and barometer fail at the same time. You find out about these things at 3am in the field when something hits the ground.
Fabric is a small compiled language where these aren't runtime bugs. They're compile-time errors.
| C++ / Python on ROS | Fabric | |
|---|---|---|
| Sensor failure handling | Runtime crash, hope the watchdog catches it | Compile error β no fallback = no build |
| Uncertainty tracking | Ignored entirely | Tracked through types, propagated through math |
| Deadline enforcement | "Should be fine" + profilers after the fact | WCET analysis at compile time, deadline miss = error |
| Fallback coverage | Manual testing, code review | BFS graph analysis, circular fallbacks caught |
| State machine correctness | Runtime panic on unhandled state | Exhaustive match checking, missing case = error |
| Sensor fusion | Write your own, pray the weights are right | merge imu gps [0.6, 0.4] β compiler checks types + uncertainty |
| Code generation | You write the hardware code | Compiler emits Python (Webots) or C (ARM) from same source |
sensor imu: Sensor<f32, Β±0.1>
sensor altitude: Sensor<f32, Β±0.5>
actuator motors: Motor[4]
when imu unavailable for 100ms {
fallback to 0.0
}
loop control within 2ms {
let fused: f32 = merge imu altitude [0.6, 0.4]
write motors[0] = fused
}
That code declares two noisy sensors with explicit error bounds, a fallback path for when the IMU dies, a control loop with a 2ms deadline, and sensor fusion with weights. The compiler checks all of it.
If you write this:
loop stabilize within 2ms {
let val: f32 = add(1.0) // wrong: add expects 2 args
}
The compiler tells you: WrongArity: expected 2 arguments, found 1. At compile time. Not when the drone is flying.
If you forget a fallback:
sensor gps: Sensor<f32, Β±1.0>
// no "when gps unavailable" handler
loop control within 2ms {
let pos: f32 = read gps // uses gps with no fallback
}
The compiler refuses to build: MissingFallback: sensor 'gps' has no fallback path. Same way Rust makes you handle every enum variant.
If your loop is too slow:
loop control within 0.5ms {
let a: f32 = read imu
let b: f32 = merge imu altitude [0.7, 0.3]
let c: f32 = complex_math(b)
// ... lots more code ...
}
The timing analyzer estimates worst-case execution time and tells you: DeadlineExceeded: loop 'control' estimated 1.2ms, deadline is 0.5ms.
sensor imu: Sensor<f32, Β±0.1>
sensor lidar: Sensor<f32, Β±0.02>
The error bound travels with the type. When you combine two sensors, the compiler tracks the combined uncertainty automatically.
let fused: f32 = merge imu altitude [0.6, 0.4]
Weighted average with combined uncertainty. merge takes at least two sensors and optional weights. The compiler computes: Β±(0.1 * 0.6 + 0.5 * 0.4) = Β±0.26.
loop stabilize within 2ms {
// body must finish within 2ms
}
WCET analysis uses IPET (Implicit Path Enumeration Technique) with an ILP solver to compute mathematically proven worst-case bounds against ARM Cortex-M4 instruction costs. If the loop can't finish in time, it's a compile error β not an estimate, a proof.
when imu unavailable for 100ms {
fallback to dead_reckoning(0.0)
}
Every sensor dependency must have an explicit fallback. The compiler builds a dependency graph and checks coverage. Missing a path = compile error. Circular fallbacks = compile error.
let mode: f32 = match gps {
ok => 1.0,
timeout => 0.5,
error => 0.0
}
Branch on sensor health explicitly. Forces you to handle every state.
let alive: bool = probe imu
if alive {
let val: f32 = read imu
}
Boolean check on sensor availability for runtime branching.
fn dead_reckoning(imu_val: f32) -> f32 {
return imu_val * 0.95
}
Parameter types, return types, arity checking, recursive calls. Works.
drone swarm {
count: 4
spacing: 2.0
formation: grid
}
Declares a drone swarm with N drones in a specified formation. Compiler generates Webots supervisor code that spawns and coordinates the swarm.
.drone.fab (source)
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LEXER (fabric-lexer) β
β logos DFA tokenizer, 55+ tokens, UTF-8 symbols β
β Input: "sensor imu: Sensor<f32, Β±0.1>" β
β Output: [Sensor, Ident("imu"), Colon, Sensor, ...] β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PARSER (fabric-parser) β
β Hand-rolled Pratt + recursive descent, 772 lines β
β Input: token stream β
β Output: Program { sensors, lets, functions, ... } β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TYPE CHECKER (fabric-types) β
β Uncertainty propagation, arity checks, return typesβ
β Catches: wrong args, type mismatches, bad merges β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SAFETY CHECKER (fabric-checker) β
β ββββββββββββββββββββ βββββββββββββββββββββββββββββββ
β β Fallback Graph β β IPET Timing Solver ββ
β β BFS cycle detect β β ILP-based WCET ββ
β β Missing = ERR β β Proven bounds = PASS ββ
β ββββββββββββββββββββ βββββββββββββββββββββββββββββββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Refinement Types (interval arithmetic) ββ
β β Proves uncertainty bounds mathematically ββ
β β Sensor fusion stays within safety limits ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CODE GENERATOR (fabric-codegen) β
β Two backends, same AST: β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Python emitter β β C emitter β β
β β Webots robotics β β ARM Cortex-M / RPi β β
β β numpy, ternary β β hal_sleep, motor_set β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
βΌ
drone.py / drone.c
(ready to run)
8 Rust crates. 4,500+ lines of Rust. 50 tests.
fabric/
crates/
fabric-core/ 146 lines Shared types, spans, error handling
fabric-ast/ 327 lines AST definitions
fabric-lexer/ 284 lines Tokenizer (logos)
fabric-parser/ 772 lines Pratt parser
fabric-types/ 447 lines Type system, uncertainty tracking
fabric-checker/ 1350 lines Fallback graph, IPET timing, refinement types
fabric-codegen/ 606 lines Python + C code generation
fabric-cli/ 550 lines CLI binary (check, build, ast, timing)
examples/
drone.fab 3 sensors, merge, match, probe, functions, fallbacks
stabilize.fab PID controller with sensor fusion
tools/
fabric-run.py Compiles .fab and sets up Webots project
webots/
worlds/ Webots world files
The same .fab file compiles to both Python (for Webots simulation) and C (for ARM hardware).
Python output (drone.py):
class FabricController(Robot):
def run(self):
while self.step(self.timeStep) != -1:
imu_raw = self.imu.getValue()
altitude_raw = self.altitude.getValue()
# Fallback checks
if imu_raw is not None:
imu_last_known = imu_raw
elif self.getTime() - imu_timeout_start > imu_TIMEOUT:
imu_fallback_active = True
# Sensor fusion
fused = ((imu * 0.60) + (altitude * 0.40))
# State-dependent control
mode = (1.00 if gps is not None else (0.50 if timeout_gps else 0.00))
correction = (err * 0.50) * mode
self.motors[0].setVelocity(correction)C output (drone.c):
int main(void) {
init_sensors();
init_actuators();
while (1) {
uint32_t loop_start = hal_get_time_ms();
float imu_raw = sensor_read(imu_handle);
float fused = ((imu * 0.60) + (altitude * 0.40));
float mode = (!isnan(gps) ? 1.00 : (timeout_gps ? 0.50 : 0.00));
float correction = (err * 0.50) * mode;
motor_set_position(motors_handles[0], correction);
// Enforce timing deadline
uint32_t elapsed = hal_get_time_ms() - loop_start;
if (elapsed < (uint32_t)2) {
hal_sleep_ms((uint32_t)2 - elapsed);
}
}
}- Rust toolchain (1.75+)
- Python 3.10+ (for Webots runner, optional)
.\try.ps1This builds the compiler, compiles drone.fab to Python and C, runs timing analysis, and runs all 50 tests. Takes about 20 seconds on first run.
git clone https://github.com/yourusername/fabric.git
cd fabric
cargo build --releasecargo test
# 50 tests, all passing# Check for errors
./target/release/fabric-lang check --file examples/drone.fab
# Generate Python
./target/release/fabric-lang build --target python --file examples/drone.fab --output drone.py
# Generate C
./target/release/fabric-lang build --target c --file examples/drone.fab --output drone.c
# Dump AST
./target/release/fabric-lang ast --file examples/drone.fab
# Check timing (IPET analysis)
./target/release/fabric-lang timing --file examples/drone.fab --clock-mhz 72.0python tools/fabric-run.py examples/drone.fab --launchRequires Webots R2023b+ installed separately.
The test suite covers every stage of the compiler:
| What's tested | How many |
|---|---|
| Lexer tokenization | 8 tests |
| Parser correctness | 5 tests |
| Type system | 3 tests |
| Checker logic (fallbacks) | 2 tests |
| Checker logic (IPET) | 7 tests |
| Checker logic (refinement) | 4 tests |
| Codegen output | 2 tests |
| End-to-end integration | 18 tests |
| Total | 50 tests |
Integration tests parse real .fab code, type-check it, run the fallback and timing analyzers, generate Python and C, and assert the output contains correct code.
Example from the test suite:
#[test]
fn test_merge_expression() {
let source = r#"
sensor imu: Sensor<f32, Β±0.1>
sensor altitude: Sensor<f32, Β±0.5>
// ...
let fused: f32 = merge imu altitude [0.6, 0.4]
"#;
let prog = parse_source(source);
let errors = check_program(&prog, 72.0);
assert!(errors.is_empty());
let py = PythonEmitter.emit_program(&prog);
assert!(py.contains("imu * 0.6"));
assert!(py.contains("altitude * 0.4"));
}- Real hardware validation -- tested against generated code, not physical motors
The IPET (Implicit Path Enumeration Technique) timing analysis in Fabric is based on the original formulation by Y.-T. S. Li and S. Malik, "Implicit Path Enumeration Technique for Performance Analysis of Real-Time Software," Proceedings of the 15th Real-Time Systems Symposium (RTSS), 1994. The novelty in Fabric is applying IPET inside a language's own compiler pipeline tied to sensor/fallback types, not inventing IPET itself.
Why hand-rolled parser instead of chumsky/pest?
Chumsky's API changed significantly between versions and the documentation was sparse for the patterns I needed. A Pratt parser for expressions + recursive descent for statements was about 700 lines and handles everything cleanly. No dependencies to fight.
Why not LLVM?
Overkill for a DSL. The C backend generates plain C that any GCC/Clang can compile. The Python backend generates valid Webots controllers. No IR needed.
Why logos for lexing?
Logos generates a DFA-based tokenizer from attribute macros. Zero runtime overhead, handles Unicode correctly, and the callback system works well for duration literals and error bounds.
Why Rust?
The type system in Rust mirrors what I'm building in Fabric. Pattern matching for the AST, enums for error types, traits for the codegen interface. Plus cargo test is excellent.
MIT
A 17-year-old who got tired of drone code that works until it doesn't.