How BASIS enforces determinism and bounded resource use at compile time.
BASIS is designed for embedded systems where:
- memory is scarce
- runtime failures are expensive
- predictable execution matters more than expressive freedom
- foreign-function boundaries must be made explicit
The compiler is intentionally opinionated: it would rather reject code early than let uncertainty survive into firmware.
Every BASIS file must declare:
#[max_memory(256kb)]
Optional module profiles can also declare:
#[strict]
#[max_storage(4kb)]
#[max_storage_objects(16)]
#[max_task_stack(8kb)]
| Component | Description |
|---|---|
Stack |
Deepest reachable call-path stack usage |
Heap |
Bounded dynamic allocation tracked through calls and loops |
Task stack |
Sum of all @task(stack=N) reservations |
Storage use |
Persistent bytes and object counts from @storage(...) |
Code (~) |
Estimated generated code size |
======================================================================
RESOURCE ANALYSIS
======================================================================
Program Size Summary:
Stack (max): 68 bytes
Heap (total): 0 bytes
Task stack: 1024 bytes
Storage use: 512 bytes / 4 objects
Code (~): 300 bytes
-------------------------------
TOTAL: 1392 bytes (1.36 KB)
Deepest path: main -> persist_batch -> append_log
======================================================================
If a program exceeds #[max_memory(...)], #[max_storage(...)],
#[max_storage_objects(...)], or #[max_task_stack(...)], compilation fails.
whileloops are rejected outright withE_WHILE_REMOVEDforloops must have provable bounds- recursion is allowed only with
@recursion(max=N) - recursive cycles must agree on the same recursion depth
This keeps termination and stack growth analyzable.
- heap allocation sizes must be compile-time constants or bounded parameters
- heap usage is multiplied across bounded loops
- recursive functions cannot allocate heap memory
- generic foreign allocators must declare
@allocates(max=N)
Example:
@deterministic @allocates(max=96) @stack(48) extern fn reserve_dma() -> *u8;
Persistent storage is tracked separately from heap because long-running systems often fail from log growth, object creation, or other state accumulation that is not ordinary RAM allocation.
Use:
#[max_storage(4kb)]
#[max_storage_objects(16)]
@deterministic
@storage(max_bytes=128, max_objects=1)
@stack(64)
extern fn append_log(record_id: u32) -> void;
The compiler propagates storage use through the whole-program call graph and multiplies it through bounded loops.
Interrupt handlers and strict modules cannot use persistent storage.
Every extern fn must declare:
@stack(N)- exactly one of
@deterministicor@nondeterministic
Optional refinements:
@blocking@allocates(max=N)@storage(max_bytes=N, max_objects=N)@reentrant@uses_timer@may_fail@isr_safe
Example:
@deterministic @reentrant @isr_safe @stack(32) extern fn board_crc(seed: u32) -> u32;
@nondeterministic @blocking @stack(64) extern fn read_i32() -> i32;
@deterministic @storage(max_bytes=128, max_objects=1) @stack(64) extern fn append_log(id: u32) -> void;
These effects propagate through the same call graph used for stack analysis.
@interrupt functions must:
- be
public - return
void - take no parameters
- not be
extern - not be recursive
- not allocate heap
- not use persistent storage
- call only deterministic, ISR-safe, reentrant code
- not call blocking code
Violations surface as errors such as:
E_INTERRUPT_SIGNATUREE_INTERRUPT_HEAPE_INTERRUPT_STORAGEE_INTERRUPT_BLOCKINGE_INTERRUPT_NONDETERMINISTICE_INTERRUPT_UNSAFE_CALLE_INTERRUPT_REENTRANCY
@task(stack=N, priority=M) marks a function as a runtime task entry point.
Compile-time checks enforce that task functions:
- are
public - return
void - take no parameters
- are not recursive
- declare an explicit stack budget
- are not also marked
@interrupt
Task stacks are tracked separately and can be bounded with #[max_task_stack(...)].
#[strict] defines a tighter deterministic profile for a module.
Strict modules reject:
- nondeterministic calls
- blocking calls
- heap allocation
- persistent storage usage
This is useful for control logic that needs a harder guarantee boundary than the default BASIS profile.
The standard library now includes:
timefor rollover-safe tick/deadline helpersmmiofor typedvolatileregister access helpers
Example:
import time::*;
let deadline: u32 = deadline_from_u32(0xFFFF_FFF0 as u32, 32 as u32);
let reached: bool = deadline_reached_u32(0x0000_0008 as u32, deadline);
import mmio::*;
let gpio_out: volatile *u32 = 0x3FF44004 as volatile *u32;
write32(gpio_out, 1 as u32);
- explicit type annotations on variables
- compile-time type checking for expressions and returns
- no array returns by value
- explicit casts for representation changes
- compile-time and runtime array bounds enforcement where applicable
BASIS is not trying to be a fully unrestricted systems language. Its safety model comes from restricting the language until stack usage, heap usage, persistent storage, interrupts, tasks, and foreign-call behavior are all more visible at compile time.