7.7 KiB
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;