Files
DSA/doc/notes/dscs_reference_draft.md
T
2026-08-20 22:22:09 +01:00

7.7 KiB

DSCS reference (Draft v0.2)

DSCS - Damn Simple Code (Systems)

Design goals

  • a simple, explicit systems language with all the capabilities of a language like C, with improved safety and ergonomics.

Lexical structure

// single line comment...
/* multi 
  line 
  comment */

// variables:
fn main() { // function names
    let local: Type; // local variables
}

const CONST: Type = val; // strictly cannot change, computed at compile time.
static STATIC: Type = val; // a global value that can change. fixed size.

fn types() {
    let boolean: bool = false; // or true;
    let integer: u32 = 37; // or i32, i16, i8, u16, u8;
    let hex: u8 = 0xAA;
    let oct: u8 = 0o5;
    let binary: u8 = 0b01000011;

    // declare an array as so:
    // because the size is stored with the type, the compiler can infer the length
    let arr: [u32; 6] = [0, 1, 2, 3, 4, 5];
    // = [1; 6]; // alternative syntax for [1, 1, 1, 1, 1, 1]

    // pointers. these are not references so care should be taken to use them safely.
    let ptr: * u32 = &integer;
    let mut_ptr: *mut u32 = &mut integer;
}

// structs
struct Point { 
    // declare a field with the "field" keyword, and end with a semicolon
    field x: u32;
    field y: u32;
    
    // make a field public with the pub keyword.
    pub field id: u32;
    
    // consts can also be stored within struct definitions. 
    // this associated constant can be accessed with Vec2::SOME_SIZE
    const SOME_SIZE: u32 = 0;
    
    // you can also use associated statics.
    // as with statics in other languages, 
    // you should use an atomic type when dealing with them in concurrent code, 
    // even on a single core architecture like DSA.
    static COUNT: u32 = 0;
    
    pub fn new(x: u32, y: u32) -> Self {
        // not thread safe!
        let id = COUNT;
        COUNT += 1;
        
        Self {
            x,
            y,
            id
        }
    }
    
    // methods can be declared inside struct definitions.
    // methods starting with a *self access the struct immutably
    pub fn get_x(*self) -> u32 {
        // implicit return on the last expression of a function body;
        self.x 
    }

    // the *mut self argument means the method can mutate the instance of the struct it is performed on.
    pub fn set_y(*mut self, y: u32) {
        self.y = y;
    }

    // returns a unit vector from the origin in the direction of the point.
    // taking self as a parameter implicitly moves self into the function body.
    // this means the value is no longer in the scope of the caller and is therefore inaccessible. 
    // any pointers to the value that still exist may be dangling and should not be used.
    pub fn unit(self) -> Self {
        Self {
            x: self.x / (self.x + self.y),
            y: self.y / (self.x + self.y),
        }
    }
}

// you can use enums to represent various different types at the same time.
// useful for state machines.
enum Shape {
    variant Circle(u32);
    variant Rectangle(u32, u32);
    variant Square(u32);
    variant Point; // doesn't have a size so we use the unit variant.

    const PI: u32 = 3; // the lack of floating point values isn't ideal.

    pub fn is_square(*self) -> bool {
        // pattern matching with match!
        match *self {
            Self::Square(_) => true,
            _ => false,
        }
    }
}

// DSCS also supports simple generics with static dispatch.
// below is an example of a struct using generics, that also uses heap memory.
struct Vec<T> {
    field data: *mut T;
    field capacity: u32;
    field length: u32;
    
    // with_capacity takes a generic T and returns an instance of Self with that type T.
    // DSCS is a language with manual memory management, so you must pass an allocator whenever you intend to
    // allocate dynamic memory.
    fn with_capacity<T>(alloc: *mut Allocator, capacity: u32) -> Self<T> {
        let ptr = alloc.alloc(capacity) as *mut T;
        Self {
            data: ptr,
            capacity,
            length: 0
        }
    }
    
    fn length(*self) -> u32 {
        self.length
    }
    
    fn resize(*mut self, alloc: *mut Allocator, new_size: u32) {
        self.data = alloc.realloc(self.data, u32);
    }
    
    // consumes self and frees its memory.
    fn drop(self, alloc: *mut Allocator) {
        alloc.free(self.data);
    }
}

// in the above example, allocator is also a statically dispatched opaque type.
// the Allocator given could essentially be anything. all we know as the user is that the allocator implements the Allocator trait:

// traits

trait Allocator {
    // allocates a new block of memory of the given size and returns a pointer to it.
    // a void pointer must be explicitly cast by the caller to the type that they intend to use it as, before it can be dereferenced.
    // dereferencing a void pointer is a compiler error.
    fn alloc(*mut self, size: u32) -> *mut void;
    
    // frees the memory allocated to `ptr`
    // this also sets the pointer to a null pointer so that the user can check it has been freed.
    // while we cannot set it to zero by dereferencing it, 
    // we can cast it to a *null which implicitly sets the pointer to zero.
    fn free(*mut self, ptr: *mut *void);
    
    // this will probably also move the memory at that pointer to the new location.
    fn realloc(*mut self, ptr: *mut void);
}

trait DivisibleByN<T: Integer> {
    fn is_divisible(*self, n: u32) {
        // the integer_div method belongs to the Integer trait.
        self.integer_div(n) == 0;
    }
}

// memory management
fn main() {
    // we have a few different ways to manage memory.
    let mut alloc = ArenaAllocator::new(HEAP_START);
    
    // method 1
    // the pattern below is commonly used in C, however it's strongly discouraged in this language!
    // the data is not a valid instance of SomeType, and is not guaranteed to be zero initialised.
    // please don't do this, it makes it very easy for UB to happen!
    let ptr = alloc.alloc(Point::size()) as *mut Point; 
    ptr.id = 1; 
    
    alloc.free(ptr); // don't use pointer now!
    
    // method 2: put it in a box!
    let mut point = Box::new(Point::new(2, 3));
    // Box::drop(self, alloc) uses the passed allocator to free its contents when called.
    // deferring execution of a function means it is executed as soon as the enclosing function returns. 
    defer ptr.drop(alloc); 
                            

    // Box is implemented as a transparent type so we can simply use Box<T> as if it were a *mut T!
    point.set_y(5);
}

// more traits
trait Task {
    type Output;
    fn poll(*mut self) -> Poll<Self::Output>;
}

struct Keyboard;
impl Task for Keyboard {
    type Output = u8;
    fn poll(*mut self) -> Poll<Self::Output> {
        if KEYBOARD.has_byte() { Poll::Ready(KEYBOARD.read()) } else { Poll::Pending }
    }
}

// function types!
fn add(x: u32, y: u32) -> u32 {
    return x + y;
}


// in systems level programming we sometimes need to write ISRs to handle interrupts.
fn invalid_instruction_fault() {
    // handle the interrupt    
}
// but we can't just put a function in our code and expect interrupts to go to it. 
// the machine needs to know where to find our interrupt handlers.

// this means functions also need to be a type!
// C got around this using void pointers, but we can do better here.

// a function pointer has the type signature `Func<A, E>` with the extra hardcoded constraint that A must be a tuple of values.
let handler: Func<(), void> = invalid_instruction_fault;
let func: Func<(y32, u32), u32> = add;




// like rust, error handling is done with Result<T, E> and optionals with Option<T>.
// an owned value in DSCS cannot be a null or none type, however a pointer can be, if explicitly cast to *null;