A safer C for embedded systems development. Transpiles to clean, readable C.
npm install c-next





A safer C for embedded systems development. Transpiles to clean, readable C.
Status: Working Transpiler — Verified on Teensy MicroMod hardware.
``cnx
// Register binding with type-safe access
register GPIO7 @ 0x42004000 {
DR: u32 rw @ 0x00,
DR_SET: u32 wo @ 0x84,
DR_TOGGLE: u32 wo @ 0x8C,
}
u32 LED_BIT <- 3;
scope LED {
void toggle() {
// Type-aware bit indexing on write-only register
GPIO7.DR_TOGGLE[LED_BIT] <- true;
}
}
`
Generates clean C:
`c
#define GPIO7_DR_TOGGLE ((volatile uint32_t)(0x42004000 + 0x8C))
uint32_t LED_BIT = 3;
void LED_toggle(void) {
GPIO7_DR_TOGGLE = (1 << LED_BIT);
}
`
`bash`
npm install -g c-next
Verify the installation:
`bash`
cnext --version
`bash`
git clone https://github.com/jlaustill/c-next.git
cd c-next
npm install
npm link
`bashTranspile to C (output alongside input file)
cnext examples/blink.cnx
VS Code Extension
The C-Next VS Code extension provides syntax highlighting, live C preview, IntelliSense, and error diagnostics.
Install from: VS Code Marketplace (coming soon)
Source: github.com/jlaustill/vscode-c-next
Getting Started with PlatformIO
C-Next integrates seamlessly with PlatformIO embedded projects. The transpiler automatically converts
.cnx files to .c before each build.$3
From your PlatformIO project root:
`bash
cnext --pio-install
`This command:
- Creates
cnext_build.py (pre-build transpilation script)
- Modifies platformio.ini to add extra_scripts = pre:cnext_build.py$3
1. Create
.cnx files in your src/ directory (alongside existing .c/.cpp files)`bash
src/
├── main.cpp # Existing C++ code
├── ConfigStorage.cnx # New c-next code
└── SensorProcessor.cnx # New c-next code
`2. Build as usual — transpilation happens automatically:
`bash
pio run
`Output:
`
Transpiling 2 c-next files...
✓ ConfigStorage.cnx
✓ SensorProcessor.cnx
Building...
`3. Commit both
.cnx and generated .c files to version control$3
Generated
.c files are reviewable artifacts in pull requests:`diff
+ // ConfigStorage.cnx
+ u8 validate_config() {
+ counter +<- 1;
+ }+ // ConfigStorage.c (generated)
+ uint8_t validate_config(void) {
+ counter = cnx_clamp_add_u8(counter, 1);
+ }
`Benefits:
- See exactly what C code the transpiler generates
- Review safety features (overflow protection, atomic operations)
- Verify transpiler behavior
- Build succeeds even if transpiler isn't available
This follows the same pattern as TypeScript committing
.js files or Bison committing generated parsers.$3
`
my-teensy-project/
├── platformio.ini # PlatformIO config
├── cnext_build.py # Auto-generated transpilation script
├── src/
│ ├── main.cpp # C++ entry point
│ ├── ConfigStorage.cnx # c-next source
│ ├── ConfigStorage.c # Generated (committed)
│ ├── SensorProcessor.cnx # c-next source
│ └── SensorProcessor.c # Generated (committed)
└── include/
└── AppConfig.h # Shared types
`$3
To remove c-next integration:
`bash
cnext --pio-uninstall
`This removes:
-
cnext_build.py script
- extra_scripts reference from platformio.iniYour
.cnx files and generated .c files remain untouched.$3
If you prefer manual control, you can also run the transpiler explicitly:
`bash
Transpile all .cnx files in a directory (recursive)
cnext src/Or transpile specific files
cnext src/ConfigStorage.cnx
cnext src/SensorProcessor.cnx
`Philosophy
C-Next follows the TypeScript model for adoption:
1. Not all-or-nothing — Drop a single
.cnx file into an existing C project
2. Clean escape hatch — Generated C is idiomatic and maintainable
3. Helpful, not burdensome — If you know C, you can read C-Next immediately$3
KISS (Keep It Simple, Stupid)
Every feature must pass the simplicity test: "Can a senior C developer read this cold and understand it in 30 seconds?" If not, it's too clever.
DRY (Don't Repeat Yourself)
Configuration belongs in one place. No magic numbers scattered through code. Named constants and register bindings enforce single sources of truth.
Pragmatic, Not Dogmatic
C-Next makes the right thing easy and the wrong thing hard, but doesn't prevent escape hatches. Generated C is always readable and maintainable.
$3
C-Next uses the standard C preprocessor — no custom module system. This means:
-
#include directives pass through to generated C
- Include C-Next files: #include "utils.cnx" → #include "utils.h" in generated C
- Works with both and "file.cnx" syntax
- MISRA preprocessor guidelines apply
- Full compatibility with existing toolchains (PlatformIO, arm-gcc, etc.)
- Conditional compilation (#ifdef) works as expectedGenerated headers automatically include guards:
`c
#ifndef MYFILE_H
#define MYFILE_H
// ...
#endif / MYFILE_H /
`$3
| Rust's Path | C-Next's Path |
| ---------------------------- | --------------------------------------- |
| Add concepts to catch errors | Remove the ability to make errors |
| Borrow checker complexity | Startup allocation = predictable memory |
| Lifetime annotations | Fixed runtime layout = clear lifetimes |
|
unsafe escape hatch | Clean C is the escape hatch |Guiding Principle: If Linus Torvalds wouldn't approve of the complexity, it doesn't ship. Safety through removal, not addition.
Core Features
$3
Eliminates the
if (x = 5) bug by design:`cnx
x <- 5; // assignment: value flows INTO x
if (x = 5) // comparison: single equals, just like math
`$3
`cnx
u8, u16, u32, u64 // unsigned integers
i8, i16, i32, i64 // signed integers
f32, f64 // floating point
bool // boolean
`$3
Type-safe hardware access with access modifiers:
`cnx
register GPIO7 @ 0x42004000 {
DR: u32 rw @ 0x00, // Read-Write
PSR: u32 ro @ 0x08, // Read-Only
DR_SET: u32 wo @ 0x84, // Write-Only (atomic set)
DR_CLEAR: u32 wo @ 0x88, // Write-Only (atomic clear)
DR_TOGGLE: u32 wo @ 0x8C, // Write-Only (atomic toggle)
}
`$3
Integers are indexable as bit arrays:
`cnx
u8 flags <- 0;
flags[3] <- true; // Set bit 3
flags[0, 3] <- 5; // Set 3 bits starting at bit 0
bool isSet <- flags[3]; // Read bit 3// .length property
u8 buffer[16];
buffer.length; // 16 (array element count)
flags.length; // 8 (bit width of u8)
`Write-only registers generate optimized code:
`cnx
GPIO7.DR_SET[LED_BIT] <- true; // Generates: GPIO7_DR_SET = (1 << LED_BIT);
`$3
Multi-byte copying with compile-time validated
memcpy generation (Issue #234):`cnx
u8 buffer[256];
u32 magic <- 0x12345678;// Copy 4 bytes from value into buffer at offset 0
buffer[0, 4] <- magic;
// Named offsets using const variables
const u32 HEADER_OFFSET <- 0;
const u32 DATA_OFFSET <- 8;
buffer[HEADER_OFFSET, 4] <- magic;
buffer[DATA_OFFSET, 8] <- timestamp;
`Transpiles to direct memcpy (bounds validated at compile time):
`c
uint8_t buffer[256] = {0};
uint32_t magic = 0x12345678;memcpy(&buffer[0], &magic, 4);
memcpy(&buffer[8], ×tamp, 8);
`Key Features:
- Compile-time bounds checking prevents buffer overflows at compile time
- Offset and length must be compile-time constants (literals or
const variables)
- Silent runtime failures are now compile-time errors
- Works with struct fields: buffer[0, 4] <- config.magic
- Distinct from bit operations: array slices use memcpy, scalar bit ranges use bit manipulation$3
Organize code with automatic name prefixing. Inside scopes, explicit qualification is required:
-
this.X for scope-local members
- global.X for global variables, functions, and registers`cnx
const u8 LED_BIT <- 3;scope LED {
u8 brightness <- 100;
void on() { global.GPIO7.DR_SET[global.LED_BIT] <- true; }
void off() { global.GPIO7.DR_CLEAR[global.LED_BIT] <- true; }
u8 getBrightness() { return this.brightness; }
}
// Call as:
LED.on();
LED.off();
`Transpiles to:
`c
const uint8_t LED_BIT = 3;static uint8_t LED_brightness = 100;
void LED_on(void) { GPIO7_DR_SET = (1 << LED_BIT); }
void LED_off(void) { GPIO7_DR_CLEAR = (1 << LED_BIT); }
uint8_t LED_getBrightness(void) { return LED_brightness; }
`$3
Safe switch with MISRA compliance:
- Braces replace break (no colons needed)
- No fallthrough allowed
- Multiple cases with
|| syntax
- Counted default(n) for enum exhaustiveness`cnx
enum EState { IDLE, RUNNING, STOPPED }void handleState(EState state) {
switch (state) {
case EState.IDLE {
startMotor();
}
case EState.RUNNING || EState.STOPPED {
checkSensors();
}
}
}
`Transpiles to:
`c
switch (state) {
case EState_IDLE: {
startMotor();
break;
}
case EState_RUNNING:
case EState_STOPPED: {
checkSensors();
break;
}
}
`$3
Safe conditional expressions with MISRA compliance:
- Parentheses required around condition
- Condition must be boolean (comparison or logical)
- No nesting allowed (use if/else instead)
`cnx
u32 max <- (a > b) ? a : b;
u32 abs <- (x < 0) ? -x : x;
u32 result <- (a > 0 && b > 0) ? a : b;// ERROR: Condition must be boolean
// u32 bad <- (x) ? 1 : 0;
// ERROR: Nested ternary not allowed
// i32 sign <- (x > 0) ? 1 : (x < 0) ? -1 : 0;
`$3
Safe, statically-allocated strings with compile-time capacity checking:
`cnx
string<64> name <- "Hello"; // 64-char capacity, transpiles to char[65]
string<128> message; // Empty string, initialized to ""
const string VERSION <- "1.0.0"; // Auto-sized to string<5>// Properties
u32 len <- name.length; // Runtime: strlen(name)
u32 cap <- name.capacity; // Compile-time: 64
// Comparison - uses strcmp
if (name = "Hello") { } // strcmp(name, "Hello") == 0
// Concatenation with capacity validation
string<32> first <- "Hello";
string<32> second <- " World";
string<64> result <- first + second; // OK: 64 >= 32 + 32
// Substring extraction with bounds checking
string<5> greeting <- name[0, 5]; // First 5 chars
`All operations are validated at compile time:
- Literal overflow → compile error
- Truncation on assignment → compile error
- Concatenation capacity mismatch → compile error
- Substring out of bounds → compile error
$3
Type-safe function pointers with the Function-as-Type pattern:
- A function definition creates both a callable function AND a type
- Nominal typing: type identity is the function name, not just signature
- Never null: callbacks are always initialized to their default function
`cnx
// Define callback type with default behavior
void onReceive(const CAN_Message_T msg) {
// default: no-op
}struct Controller {
onReceive _handler; // Type is onReceive, initialized to default
}
// User implementation must match signature
void myHandler(const CAN_Message_T msg) {
Serial.println(msg.id);
}
controller._handler <- myHandler; // OK: signature matches
controller._handler(msg); // Always safe - never null
`Transpiles to:
`c
void onReceive(const CAN_Message_T msg) { }typedef void (*onReceive_fp)(const CAN_Message_T);
struct Controller {
onReceive_fp _handler;
};
// Initialization always sets to default
struct Controller Controller_init(void) {
return (struct Controller){ ._handler = onReceive };
}
`$3
ISR-safe variables with hardware-assisted atomicity:
`cnx
#pragma target teensy41atomic u32 counter <- 0; // ISR-safe with LDREX/STREX
atomic clamp u8 brightness <- 100; // Combines atomic + clamp
void increment() {
counter +<- 1; // Lock-free atomic increment
}
`Generates optimized code based on target platform:
- Cortex-M3/M4/M7: LDREX/STREX retry loops (lock-free)
- Cortex-M0/M0+: PRIMASK disable/restore (interrupt masking)
Target detection priority:
--target CLI flag > platformio.ini > #pragma target > default$3
Prevent compiler optimization for variables that change outside normal program flow:
`cnx
// Delay loop - prevent optimization
void delay_ms(const u32 ms) {
volatile u32 i <- 0;
volatile u32 count <- ms * 2000; while (i < count) {
i +<- 1; // Compiler cannot optimize away
}
}
// Hardware register - reads actual memory
volatile u32 status_register @ 0x40020000;
void waitReady() {
while (status_register & 0x01 = 0) {
// Always reads from hardware
}
}
`When to use:
- ✅ Delay loops that must not be optimized away
- ✅ Memory-mapped hardware registers
- ✅ Variables polled in tight loops
- ❌ ISR-shared variables (use
atomic instead for RMW safety)Key difference from
atomic:-
volatile = prevents optimization only
- atomic = prevents optimization + adds synchronization (ISR-safe)$3
Multi-statement atomic blocks with automatic interrupt masking:
`cnx
u8 buffer[64];
u32 writeIdx <- 0;void enqueue(u8 data) {
critical {
buffer[writeIdx] <- data;
writeIdx +<- 1;
}
}
`Transpiles to PRIMASK save/restore:
`c
void enqueue(uint8_t data) {
{
uint32_t __primask = __get_PRIMASK();
__disable_irq();
buffer[writeIdx] = data;
writeIdx += 1;
__set_PRIMASK(__primask);
}
}
`Safety:
return inside critical { } is a compile error (E0853).$3
Safe interop with C stream functions that can return NULL:
`cnx
#include string<64> buffer;
void readInput() {
// NULL check is REQUIRED - compiler enforces it
if (fgets(buffer, buffer.size, stdin) != NULL) {
printf("Got: %s", buffer);
}
}
`Constraints:
- NULL only valid in comparison context (
!= NULL or = NULL)
- Only whitelisted stream functions: fgets, fputs, fgetc, fputc
- Cannot store C pointer returns in variables
- fopen, malloc, etc. are errors (see ADR-103 for future FILE\* support)$3
Allocate at startup, run with fixed memory. Per MISRA C:2023 Dir 4.12: all memory is allocated during initialization, then forbidden. No runtime allocation means no fragmentation, no OOM, no leaks.
Hardware Testing
Verified on Teensy MicroMod (NXP i.MX RT1062):
`bash
Build and flash with PlatformIO
cd test-teensy
pio run -t upload
`See
examples/blink.cnx for the complete LED blink example.Projects Using C-Next
| Project | Description |
| ----------------------------------------- | -------------------------------------------------------------------------- |
| OSSM | Open-source stroke machine firmware using C-Next for safe embedded control |
_Using C-Next in your project? Open an issue to get listed!_
Project Structure
`
c-next/
├── grammar/CNext.g4 # ANTLR4 grammar definition
├── src/
│ ├── codegen/CodeGenerator.ts # Transpiler core
│ ├── parser/ # Generated ANTLR parser
│ └── index.ts # CLI entry point
├── examples/
│ ├── blink.cnx # LED blink (Teensy verified)
│ └── bit_test.cnx # Bit manipulation tests
├── test-teensy/ # PlatformIO test project
└── docs/decisions/ # Architecture Decision Records
`Architecture Decision Records
Decisions are documented in
/docs/decisions/:$3
| ADR | Title | Description |
| --------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------- |
| ADR-001 | Assignment Operator |
<- for assignment, = for comparison |
| ADR-003 | Static Allocation | No dynamic memory after init |
| ADR-004 | Register Bindings | Type-safe hardware access |
| ADR-006 | Simplified References | Pass by reference, no pointer syntax |
| ADR-007 | Type-Aware Bit Indexing | Integers as bit arrays, .length property |
| ADR-010 | C Interoperability | Unified ANTLR parser architecture |
| ADR-011 | VS Code Extension | Live C preview with syntax highlighting |
| ADR-012 | Static Analysis | cppcheck integration for generated C |
| ADR-013 | Const Qualifier | Compile-time const enforcement |
| ADR-014 | Structs | Data containers without methods |
| ADR-015 | Null State | Zero initialization for all variables |
| ADR-016 | Scope | this./global. explicit qualification |
| ADR-017 | Enums | Type-safe enums with C-style casting |
| ADR-030 | Define-Before-Use | Functions must be defined before called |
| ADR-037 | Preprocessor | Flag-only defines, const for values |
| ADR-043 | Comments | Comment preservation with MISRA compliance |
| ADR-044 | Primitive Types | Fixed-width types with clamp/wrap overflow |
| ADR-024 | Type Casting | Widening implicit, narrowing uses bit indexing |
| ADR-022 | Conditional Expressions | Ternary with required parens, boolean condition, no nesting |
| ADR-025 | Switch Statements | Safe switch with braces, \|\| syntax, counted default(n) |
| ADR-029 | Callbacks | Function-as-Type pattern with nominal typing |
| ADR-045 | Bounded Strings | string with compile-time safety |
| ADR-023 | Sizeof | Type/value size queries with safety checks |
| ADR-027 | Do-While | do { } while () with boolean condition (E0701) |
| ADR-032 | Nested Structs | Named nested structs only (no anonymous) |
| ADR-035 | Array Initializers | [1, 2, 3] syntax with [0*] fill-all |
| ADR-036 | Multi-dim Arrays | arr[i][j] with compile-time bounds enforcement |
| ADR-040 | ISR Type | Built-in ISR type for void(void) function pointers |
| ADR-034 | Bitmap Types | bitmap8/bitmap16/bitmap32 for portable bit-packed data |
| ADR-048 | CLI Executable | cnext command with smart defaults |
| ADR-049 | Atomic Types | atomic keyword with LDREX/STREX or PRIMASK fallback |
| ADR-050 | Critical Sections | critical { } blocks with PRIMASK save/restore |
| ADR-108 | Volatile Variables | volatile keyword prevents compiler optimization |
| ADR-046 | Nullable C Interop | c_ prefix for nullable C pointer types (supersedes ADR-047) |
| ADR-047 | NULL for C Interop | NULL keyword for C stream functions (superseded by ADR-046) |
| ADR-052 | Safe Numeric Literals | type_MIN/type_MAX constants + safe hex conversion |
| ADR-053 | Transpiler Pipeline | Unified multi-pass pipeline with header symbol extraction |
| ADR-057 | Implicit Scope Resolution | Bare identifiers resolve local → scope → global |$3
| ADR | Title | Description |
| ------------------------------------------------------------ | ----------------------------- | --------------------------------------- |
| ADR-008 | Language-Level Bug Prevention | Top 15 embedded bugs and prevention |
| ADR-009 | ISR Safety | Safe interrupts without
unsafe blocks |$3
| ADR | Title | Description |
| --------------------------------------------------------------- | -------------------------- | --------------------------------------- |
| ADR-100 | Multi-Core Synchronization | ESP32/RP2040 spinlock patterns |
| ADR-101 | Heap Allocation | Dynamic memory for desktop targets |
| ADR-102 | Critical Section Analysis | Complexity warnings and cycle analysis |
| ADR-103 | Stream Handling | FILE\* and fopen patterns for file I/O |
| ADR-104 | ISR-Safe Queues | Producer-consumer patterns for ISR/main |
| ADR-105 | Prefixed Includes | Namespace control for includes |
| ADR-106 | Vector Table Bindings | Register bindings for ISR vector tables |
$3
| ADR | Title | Description |
| ---------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
| ADR-041 | Inline Assembly | Write assembly in C files; C-Next transpiles to C anyway |
| ADR-042 | Error Handling | Works with existing features (enums, pass-by-reference, struct returns) |
| ADR-039 | Null Safety | Emergent from ADR-003 + ADR-006 + ADR-015; no additional feature needed |
| ADR-020 | Size Type | Fixed-width types are more predictable than platform-sized |
| ADR-019 | Type Aliases | Fixed-width primitives already solve the problem |
| ADR-021 | Increment/Decrement | Use
+<- 1 instead; separation of concerns |
| ADR-002 | Namespaces | Replaced by scope keyword (ADR-016) |
| ADR-005 | Classes | Use structs + free functions instead (ADR-016) |
| ADR-018 | Unions | Use ADR-004 register bindings or explicit byte manipulation |
| ADR-038 | Static/Extern | Use scope for visibility; no static keyword in v1 |
| ADR-026 | Break/Continue | Use structured loop conditions instead |
| ADR-028 | Goto | Permanently rejected; use structured alternatives |
| ADR-031 | Inline Functions | Trust compiler; inline is just a hint anyway |
| ADR-033 | Packed Structs | Use ADR-004 register bindings or explicit serialization |Development
$3
`bash
Clone and install (IMPORTANT: npm install sets up pre-commit hooks)
git clone https://github.com/jlaustill/c-next.git
cd c-next
npm install # Installs dependencies and Husky pre-commit hooks
`Pre-commit hooks: The project uses Husky to automatically format code (Prettier) and fix linting (ESLint) before every commit. This prevents formatting errors in PRs.
$3
`bash
npm run antlr # Regenerate parser from grammar
npm run typecheck # Type-check TypeScript (no build required)
npm test # Run all tests
npm test -- --quiet # Minimal output (errors + summary only)
npm test -- tests/enum # Run specific directory
npm test -- tests/enum/my.test.cnx # Run single test fileCode quality (auto-run by pre-commit hooks)
npm run prettier:fix # Format all code
npm run eslint:check # Check for lint errorsCoverage tracking
npm run coverage:check # Feature coverage report
npm run coverage:grammar # Grammar rule coverage (generates GRAMMAR-COVERAGE.md)
npm run coverage:grammar:check # Grammar coverage with threshold check (CI)
`Note: C-Next runs directly via
tsx without a build step. The typecheck command validates types only and does not generate any output files.$3
The project includes a Prettier plugin for formatting
.cnx files with consistent style (4-space indentation, same-line braces).`bash
Format a single file
npx prettier --plugin ./prettier-plugin/dist/index.js --write myfile.cnxFormat all .cnx files in tests/
npx prettier --plugin ./prettier-plugin/dist/index.js --write "tests/*/.cnx"
`To build the plugin from source (after making changes):
`bash
cd prettier-plugin
npm install
npm run build
`Contributing
See CONTRIBUTING.md for the complete development workflow, testing requirements, and PR process.
Quick start: Ideas and feedback welcome via issues.
License
MIT
Acknowledgments
- The R community for proving
<-` works in practice