Memory Management
Nic gives you explicit control over memory. There's no garbage collector—you decide when to allocate and deallocate.
Stack vs Heap
Stack allocation is automatic and fast. Variables are cleaned up when they go out of scope:
nic
struct Point {
x: f64,
y: f64
}
fn stack_example() -> unit {
let x: i32 = 42; // stack allocated
let arr: [10]i32; // stack allocated array
let p: Point = Point { x: 1.0, y: 2.0 }; // stack allocated struct
// All automatically cleaned up when function returns
return;
}Heap allocation requires explicit new and release:
nic
fn heap_example() -> unit {
let p: *Point = new Point { x: 1.0, y: 2.0 }; // heap allocated struct
// Use the value...
p.x = 10.0;
release p; // must free explicitly
return;
}The new Keyword
new allocates on the heap and returns a pointer:
nic
struct Point {
x: f64,
y: f64
}
fn main() -> unit {
// Allocate struct with field values
let p: *Point = new Point { x: 1.0, y: 2.0 };
// Allocate class (calls init if defined)
let c: *Counter = new Counter();
// Allocate with constructor arguments
let conn: *Connection = new Connection(8080);
release p;
release c;
release conn;
return;
}The release Keyword
release frees heap memory. For classes, it calls deinit first:
nic
fn main() -> unit {
let p: *Point = new Point { x: 0.0, y: 0.0 };
// ... use p ...
release p; // memory freed
// WARNING: using p after release is undefined behavior!
// p.x = 1.0; // DON'T DO THIS
return;
}The defer Statement
defer schedules cleanup to run when the current scope exits:
nic
fn with_defer() -> unit {
let p: *Point = new Point { x: 1.0, y: 2.0 };
defer release p; // will run at end of function
// Safe to use p here...
p.x = 10.0;
// Even with early return, defer runs
if p.x > 5.0 {
return; // defer release p runs here
}
// defer release p also runs here
return;
}LIFO Execution
Multiple defers execute in reverse order (Last In, First Out):
nic
fn multiple_defers() -> unit {
let a: *Point = new Point { x: 1.0, y: 1.0 };
defer release a; // runs third
let b: *Point = new Point { x: 2.0, y: 2.0 };
defer release b; // runs second
let c: *Point = new Point { x: 3.0, y: 3.0 };
defer release c; // runs first
// Order: release c, release b, release a
return;
}Block-Scoped Defer
Defers run at the end of their enclosing block:
nic
fn block_defer() -> unit {
let outer: *Point = new Point { x: 0.0, y: 0.0 };
defer release outer;
{
let inner: *Point = new Point { x: 1.0, y: 1.0 };
defer release inner;
// inner released here, at end of block
}
// outer still valid here
outer.x = 10.0;
// outer released here
return;
}Pointers and References
nic
fn pointers() -> unit {
let x: i32 = 42;
let ptr: *i32 = &x; // pointer to x
let val: i32 = *ptr; // dereference: 42
*ptr = 100; // modify through pointer
// x is now 100
return;
}Nil Pointers
nic
fn main() -> unit {
let ptr: *i32 = nil; // null pointer
// Check before use
if ptr != nil {
let val: i32 = *ptr;
}
return;
}Common Patterns
Resource Acquisition
nic
fn process_file(path: string) -> Result[string, string] {
let file: *File = new File(path);
defer release file;
if !file.exists() {
return Err("not found");
}
let content: string = file.read();
return Ok(content);
// file released automatically via defer
}Cleanup on Error
nic
fn safe_operation() -> Result[i32, string] {
let resource: *Resource = new Resource();
defer release resource;
let result: Result[i32, string] = resource.do_work();
match result {
Ok(v) -> return Ok(v),
Err(e) -> return Err(e)
};
// resource released in all cases
}Memory Safety Rules
- Every
newneeds arelease— Memory leaks if you forget - Don't use after release — Undefined behavior
- Don't release twice — Undefined behavior (double free)
- Use
deferfor safety — Ensures cleanup even with early returns
Summary
| Feature | Purpose |
|---|---|
new StructName { ... } | Heap allocate struct |
new ClassName() | Heap allocate class |
release p | Free memory (calls deinit for classes) |
defer stmt | Run at scope exit |
&x | Address of |
*ptr | Dereference |
nil | Null pointer |
Next
Learn about Lazy Evaluation for deferred computation.