# 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 ```rust // 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 { 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(alloc: *mut Allocator, capacity: u32) -> Self { 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 { 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 as if it were a *mut T! point.set_y(5); } // more traits trait Task { type Output; fn poll(*mut self) -> Poll; } struct Keyboard; impl Task for Keyboard { type Output = u8; fn poll(*mut self) -> Poll { 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` 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 and optionals with Option. // an owned value in DSCS cannot be a null or none type, however a pointer can be, if explicitly cast to *null; ```