BASIS is a deterministic, resource-aware systems programming language for embedded development.
BASIS is designed for firmware and embedded logic that must stay bounded, predictable, and easier to reason about as systems grow. It focuses on classes of problems that often remain hidden until integration or long running field use, such as unclear stack growth, hidden heap usage, unsafe interrupt paths, and foreign calls whose behavior is not explicit.
Today BASIS compiles to C so it can fit into existing toolchains and firmware stacks. Its goal is to provide a constrained programming model where important properties of embedded code can be checked earlier, reported clearly, and integrated into existing C based projects without requiring a completely separate runtime ecosystem.
If you are new to the project, start with LEARN.md for a guided introduction before moving to the full reference documentation.
Embedded software often compiles successfully long before it becomes easy to trust. As features accumulate, projects can become harder to reason about because resource usage, call depth, interrupt safety, and foreign boundary behavior are spread across multiple files and libraries.
BASIS approaches that problem by making these concerns part of the language and compiler model. Instead of leaving them entirely to manual review and late testing, the compiler performs static checks and resource analysis so more issues can be surfaced before code reaches hardware.
#[max_memory(256kb)]
import core::*;
import io::*;
fn main() -> i32 {
let x: i32 = abs_i32(-42);
out_i32(x);
println("");
return 0;
}
- deterministic, bounded control flow
- explicit memory budgets per module
- whole-program call-graph stack analysis
- bounded heap/resource accounting
- persistent-storage budgeting
- interrupt-safe code validation
- task entry-point validation
- strict deterministic module profiles
- rollover-safe time helpers
- MMIO-oriented
volatileregister access - bit and register helper libraries
- fixed-size checksum and byte queue libraries
- explicit foreign-function effect contracts
- C code generation for host and embedded integration
BASIS currently provides the following language and compiler capabilities:
- a complete front end with lexing, parsing, semantic analysis, type checking, constant evaluation, and C code generation
- deterministic control flow rules with
whileremoved, boundedforloops, and bounded recursion through@recursion(max=N) - explicit per-module memory budgets through
#[max_memory(...)] - whole-program resource analysis covering stack, heap, task stack, persistent storage, estimated code size, and deepest call path
- strict deterministic profiles through
#[strict] - interrupt handlers through
@interrupt - task entry points through
@task(stack=N, priority=N) - persistent storage budgeting through
#[max_storage(...)],#[max_storage_objects(...)], and@storage(...) - foreign-function contracts through annotations such as
@deterministic,@nondeterministic,@blocking,@allocates,@storage,@reentrant,@uses_timer,@may_fail, and@isr_safe - rollover-safe time helpers in
stdlib/time - volatile MMIO helpers in
stdlib/mmio - register, checksum, and fixed queue helpers in
stdlib/bits,stdlib/crc, andstdlib/ring - local examples, negative tests, and a release packaging flow for Windows
BASIS is an embedded systems language under active development, with a working compiler, a static analysis pipeline, a growing standard library, and verified source and packaged workflows.
The current standard library is intentionally small and targeted at deterministic embedded work:
corefor basic numeric helpers, boolean helpers, assertions, and swapsiofor printing and basic host-side inputmemfor explicit heap and memory operationsmathfor integer math and scaling helpersstringfor C-style string functionstimefor rollover-safe tick and deadline logicmmiofor volatile register accessbitsfor masks, packed fields, and alignmentcrcfor fixed-size checksum and CRC helpersringfor fixed-size byte queues
- Python 3
gccinPATHfor host builds
git clone https://github.com/CodeforGood1/Basis.git
cd Basispython compiler/basis.py build examples/hello.bs --runpython compiler/basis.py build examples/hello.bs --emit-cpython compiler/basis.py build examples/callgraph_demo.bs --show-resources --runpython compiler/basis.py build examples/time_demo.bs --run
python compiler/basis.py build examples/task_demo.bs --show-resources --emit-c
python compiler/basis.py build examples/storage_demo.bs --show-resources --emit-c
python compiler/basis.py build examples/mmio_demo.bs --emit-c --target esp32
python compiler/basis.py build examples/bits_demo.bs --run
python compiler/basis.py build examples/crc_demo.bs --run
python compiler/basis.py build examples/ring_demo.bs --runpowershell -NoProfile -ExecutionPolicy Bypass -File .\tests\run_local_checks.ps1To also verify the packaged Windows release flow:
powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\run_local_checks.ps1 -VerifyPackagepowershell -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -Clean -PackageThis creates a versioned distribution directory and ZIP archive containing the compiler, examples, standard library, and documentation.
If you want a shell command instead of calling Python directly, add the repo's compiler folder to your PATH and use the provided launcher script on Windows.
python compiler/basis.py build --lib examples/isr_demo.bs --emit-c --target esp32This generates C output that can be linked into an existing C or embedded firmware project.
There are two main ways to use BASIS today:
Use BASIS as a normal compiled language on your machine:
python compiler/basis.py build my_program.bs --runUse BASIS as a constrained logic layer that compiles to C:
python compiler/basis.py build --lib control.bs --emit-c --target esp32Then compile the generated C with your existing firmware stack, HAL, or SDK.
For embedded workflows, the generated C is intended to be built by your existing platform toolchain. In practice, BASIS can be used to compile analyzable application logic into C and then linked into an ESP IDF, STM32 HAL, RP2040 SDK, or other C based firmware project once the surrounding platform code is provided.
Every BASIS file declares a maximum memory budget:
#[max_memory(256kb)]
#[max_memory(32kb)]
#[max_memory(512b)]
The compiler reports stack, heap, task-stack, storage usage, code-size estimate, and deepest call path:
======================================================================
RESOURCE ANALYSIS
======================================================================
Program Size Summary:
Stack (max): 20 bytes
Heap (total): 512 bytes
Task stack: 1024 bytes
Storage use: 256 bytes / 2 objects
Code (~): 700 bytes
-------------------------------
TOTAL: 2256 bytes (2.20 KB)
Deepest path: main -> filter -> accumulate
======================================================================
- array bounds checks
- bounded recursion checks
- whole-program stack checks
- heap size checks
- persistent-storage budget checks
- interrupt validation
- task entry-point validation
- strict module validation
- explicit foreign-function contracts
| Document | Description |
|---|---|
| compiler/syntax.md | Complete language syntax reference |
| compiler/safeguards.md | Safety guarantees and compile-time checks |
| compiler/limitations.md | Known limitations and workarounds |
| LEARN.md | Guided introduction for new users |
| Error | Meaning |
|---|---|
E_INDEX_OUT_OF_BOUNDS |
Array access exceeds declared size |
E_UNBOUNDED_LOOP |
Loop without determinable bound |
E_WHILE_REMOVED |
while loops are not part of BASIS |
E_MISSING_RECURSION_ANNOTATION |
Recursive function needs @recursion(max=N) |
E_UNBOUNDED_HEAP |
Allocation size not compile-time constant |
E_EXTERN_STACK_REQUIRED |
extern fn is missing @stack(N) |
E_EXTERN_EFFECT_REQUIRED |
extern fn must declare @deterministic or @nondeterministic |
E_EXTERN_ALLOCATES_BUDGET_REQUIRED |
generic extern fn with @allocates needs @allocates(max=N) |
E_STORAGE_CONTRACT_REQUIRED |
@storage must declare bytes and/or object budgets |
E_EFFECT_CONFLICT |
incompatible effect annotations were combined |
E_INTERRUPT_SIGNATURE |
@interrupt handler has invalid signature |
E_INTERRUPT_BLOCKING |
@interrupt handler calls blocking code |
E_INTERRUPT_STORAGE |
@interrupt handler uses persistent storage |
E_TASK_SIGNATURE |
@task entry point has invalid signature |
E_TASK_STACK_REQUIRED |
@task requires an explicit stack budget |
E_TASK_INTERRUPT_CONFLICT |
function cannot be both @task and @interrupt |
E_STRICT_BLOCKING |
#[strict] module calls blocking code |
E_MISSING_RETURN |
Function must return value on all paths |
E_INVALID_RETURN_TYPE |
Cannot return arrays by value |
missing #[max_memory] |
File lacks memory directive |
Program exceeds declared memory budget |
Total usage > declared max |
@recursion(max=10)
fn factorial(n: i32) -> i32 {
if n <= 1 { return 1; }
return n * factorial(n - 1);
}
import mem::*;
fn main() -> i32 {
let buffer: *u8 = alloc_bytes(256 as u32);
free_bytes(buffer);
return 0;
}
fn sum(arr: [i32; 5]) -> i32 {
let total: i32 = 0;
for i in 0..5 {
total = total + arr[i];
}
return total;
}
compiler/
├── basis.py # Main compiler driver
├── lexer.py # Tokenization
├── parser.py # Recursive descent parser
├── ast_defs.py # AST node definitions
├── sema.py # Semantic analysis
├── typecheck.py # Type checking
├── consteval.py # Constant evaluation
├── loop_analysis.py # Loop bound analysis
├── resource_analysis.py # Resource tracking
├── codegen.py # Single-file C generation
├── module_codegen.py # Multi-module C generation
└── diagnostics.py # Error reporting
stdlib/
├── core/core.bs
├── io/io.bs
├── mem/mem.bs
├── math/math.bs
├── string/string.bs
├── time/time.bs
├── mmio/mmio.bs
├── bits/bits.bs
├── crc/crc.bs
└── ring/ring.bs
examples/
├── hello.bs
├── bits_demo.bs
├── crc_demo.bs
├── ring_demo.bs
└── test_io.bs
@deterministic @isr_safe @stack(N) extern fn IDENTIFIER(param_list?) -> type;
@deterministic @blocking @stack(N) extern fn IDENTIFIER(param_list?) -> type;
@deterministic @allocates(max=N) @stack(N) extern fn IDENTIFIER(param_list?) -> type;
@nondeterministic @blocking @stack(N) extern fn IDENTIFIER(param_list?) -> type = "symbol_name";
param_list ::= param ("," param)*
param ::= IDENTIFIER ":" type
- Allowed types: i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool, void return; raw pointers to any allowed type; pointers to structs; structs by value only if trivially C-compatible.
- Forbidden in extern signatures: arrays, slices, function pointers, opaque handles, managed/high-level types.
- Variadic functions: forbidden.
@stack(N)is required on every extern so the whole-program call graph stays bounded.- Every extern must declare exactly one determinism contract:
@deterministicor@nondeterministic. @blocking,@allocates(max=N), and@isr_safeare optional refinements.@isr_safecannot be combined with@blocking,@allocates, or@nondeterministic.- Bare
@allocatesis only valid for compiler-known allocator models such asmalloc; generic foreign allocators must use@allocates(max=N).
- Default symbol name = BASIS identifier, case-sensitive, no mangling.
- Aliasing: optional
= "symbol_name"binds to the exact C symbol literal. - No implicit decoration or namespaces.
- Always platform C ABI.
- Parameters passed per platform C ABI; no hidden args.
- Returns: scalars/pointers per C ABI; structs by value only if ABI supports it (else rejected). Hidden sret may be used by ABI; if unsupported, reject.
- No exceptions/unwinding across the boundary.
- No implicit allocation or freeing by the compiler.
- No ownership transfer by default; caller retains ownership of pointers passed/received unless explicitly documented externally.
- Foreign heap behavior is explicit:
@allocates(max=N)contributes to heap budgets, and compiler-known allocators are modeled directly at call sites. - Blocking behavior is explicit:
@blockingpropagates through the call graph and disqualifies ISR use. - Determinism across extern boundaries is explicit:
@deterministic/@nondeterministicpropagate through the call graph.
- Headers generated only for non-entry modules; entry (main) emits no header.
- Public externs appear in headers as non-static prototypes; private externs only in
.cas static prototypes. - Headers contain declarations only, never bodies. Own guard first, then standard includes, then imported module headers in lexicographic order.
public fn-> prototype in header, definition in.c(non-static).private fn-> no header prototype; definition in.cas static.extern fn-> prototype only; no definition. Public externs non-static in header; private externs static in.c. Entrymainis never extern/static and no header is emitted for it.
E_EXTERN_TYPE: invalid extern type in signature.E_EXTERN_ALIAS: invalid extern alias; expected string literal.E_EXTERN_VARIADIC: variadic externs are not supported.E_EXTERN_STRUCTRET: struct return not supported by target ABI.E_EXTERN_BODY: extern functions must not have bodies.E_EXTERN_EFFECT_REQUIRED: extern functions must declare@deterministicor@nondeterministic.E_EXTERN_ALLOCATES_BUDGET_REQUIRED: generic foreign allocators must use@allocates(max=N).E_EFFECT_CONFLICT: incompatible effect annotations were combined.
- Store extern declarations (with optional alias) in symbol table.
- Type check externs like declared functions without bodies; validate parameter/return types.
- Exclude extern bodies from resource/loop analyses; analyze call sites using explicit effect contracts.
- Externs do not create module import dependencies.
- Emit C prototypes respecting visibility and aliasing; no bodies generated.
- Lowering:
extern fn foo(a: i32) -> i32;->int32_t foo(int32_t a); - Aliasing:
extern fn local(a: i32) -> i32 = "c_symbol";->int32_t local(int32_t a) __asm__("c_symbol");(or equivalent alias mechanism; if unavailable, emit prototype namedc_symbol). - Struct pointer extern:
extern fn process(s: *MyStruct) -> i32;->int32_t process(struct MyStruct* s); - No stubs/wrappers/thunks; direct calls only; zero overhead.
- C++ ABI or name mangling.
- Automatic/reflective bindings.
- Implicit headers beyond stated rules.
- Runtime shims/trampolines/thunks.
- Foreign exception propagation or unwinding across the boundary.
- Extern symbols recorded in AST and symbol table with alias info.
- Externs participate in type checking; bodies forbidden.
- Externs excluded from resource/loop analyses beyond call-site effects.
- Extern effects participate in determinism, blocking, heap, and ISR-safety propagation.
- Public externs -> prototypes in headers; private externs -> static prototypes in
.c. - No code emitted for extern bodies; zero-overhead direct calls.
- ABI-compatible prototypes generated with correct C types and calling convention.
- Diagnostics enforced:
E_EXTERN_TYPE,E_EXTERN_ALIAS,E_EXTERN_VARIADIC,E_EXTERN_STRUCTRET,E_EXTERN_BODY,E_EXTERN_EFFECT_REQUIRED,E_EXTERN_ALLOCATES_BUDGET_REQUIRED,E_EFFECT_CONFLICT.