Skip to content

subhansh-dev/fabric

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

13 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Rust Tests Crates Targets License

Fabric

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.


Why does this exist

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.

How it compares

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

What it actually does

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.


Language features

Sensor types with uncertainty bounds

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.

Sensor fusion

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.

Deadline-checked loops

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.

Mandatory fallback paths

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.

State matching

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.

Sensor probing

let alive: bool = probe imu
if alive {
    let val: f32 = read imu
}

Boolean check on sensor availability for runtime branching.

Functions

fn dead_reckoning(imu_val: f32) -> f32 {
    return imu_val * 0.95
}

Parameter types, return types, arity checking, recursive calls. Works.

Drone swarm declarations

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.


Compiler pipeline

.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.


Project structure

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

Generated output

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);
        }
    }
}

Getting started

Prerequisites

  • Rust toolchain (1.75+)
  • Python 3.10+ (for Webots runner, optional)

Quick test

.\try.ps1

This 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.

Build

git clone https://github.com/yourusername/fabric.git
cd fabric
cargo build --release

Run tests

cargo test
# 50 tests, all passing

Compile a .fab file

# 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.0

Using the Webots runner (optional)

python tools/fabric-run.py examples/drone.fab --launch

Requires Webots R2023b+ installed separately.


Proving it works

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"));
}

What this doesn't do (yet)

  • Real hardware validation -- tested against generated code, not physical motors

IPET attribution

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.


Architecture decisions

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.


License

MIT


Built by

A 17-year-old who got tired of drone code that works until it doesn't.

About

A compiled DSL for real-time robotics. Enforces deadlines, sensor uncertainty, and fallbacks at compile time. Generates Python and C. 38 tests, 3500 lines of Rust.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors