documentation updates

This commit is contained in:
2026-08-20 22:22:09 +01:00
parent 5fd9c33bce
commit 1fe08ec3a1
10 changed files with 1670 additions and 0 deletions
+2
View File
@@ -20,6 +20,8 @@
<sourceFolder url="file://$MODULE_DIR$/tooling/bf_compiler/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/tooling/bf_compiler/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tooling/dsfl_compiler/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/tooling/dsfl_compiler/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tooling/dscs_compiler/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/tooling/dscs_compiler/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tooling/old_dsc_compiler/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tooling/dssml_framework/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.idea/caches" /> <excludeFolder url="file://$MODULE_DIR$/.idea/caches" />
<excludeFolder url="file://$MODULE_DIR$/doc/.obsidian" /> <excludeFolder url="file://$MODULE_DIR$/doc/.obsidian" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
View File
View File
+782
View File
@@ -0,0 +1,782 @@
# DSCS & DSCX Language Specification (Draft v0.1)
**DSCS** (Damn Simple C — Systems) and **DSCX** (Damn Simple C — eXtended) — a two-tier
language pair for the DSA architecture. DSCS is a small, explicit systems language for
kernels and drivers. DSCX is a Rust/Kotlin-flavored OOP language that transpiles to DSCS.
Every valid DSCS program is a valid DSCX program.
---
## Table of Contents
1. [Introduction](#introduction)
2. [Lexical Structure](#lexical-structure)
3. [DSCS: Types](#dscs-types)
4. [DSCS: Ownership](#dscs-ownership)
5. [DSCS: Structs, Traits, Generics](#dscs-structs-traits-generics)
6. [DSCS: Functions, Control Flow, Memory](#dscs-functions-control-flow-memory)
7. [DSCS: Concurrency (Task/Poll)](#dscs-concurrency)
8. [DSCX: Classes and Inheritance](#dscx-classes-and-inheritance)
9. [DSCX: Properties](#dscx-properties)
10. [DSCX: Trailing Lambdas](#dscx-trailing-lambdas)
11. [DSCX: Pattern Matching and `is`](#dscx-pattern-matching-and-is)
12. [DSCX: Optionals](#dscx-optionals)
13. [DSCX: Objects (Singletons) and Coroutines](#dscx-objects-and-coroutines)
14. [Example Programs](#example-programs)
15. [Grammar Summary (partial)](#grammar-summary)
16. [Appendix A: DSCS vs DSCX vs Rust](#appendix-a-dscs-vs-dscx-vs-rust)
---
## 1. Introduction
### Design goals
- **DSCS is the floor.** It compiles to an IR, which lowers to DSA assembly. It has no
runtime, no hidden allocation, no dynamic dispatch. If you can write it in DSCS, you
can point at the exact assembly it becomes.
- **DSCX is the ceiling.** It parses to its own AST, type-checks with full inference,
then transpiles to DSCS source (or DSCS-level IR) before continuing through the same
backend. DSCX never bypasses DSCS — everything DSCX does must be expressible as
ordinary DSCS underneath.
- **DSCS ⊂ DSCX.** A `.dscs` file is always a legal `.dscx` file. Dropping to manual,
explicit low-level code is always available, never a special mode.
### Two-language boundary, one detail that matters
DSCS functions returning owned memory use `Box<T>` (owned, linear) or `&T` (borrowed) in
their signatures — never a bare untyped pointer for anything crossing a function
boundary. This is what lets DSCX's compiler automatically decide how to manage memory
it receives from DSCS calls, without documentation or guesswork (see
[§4](#dscs-ownership)).
---
## 2. Lexical Structure
Comments, identifiers, and literals are unchanged from the original DSC spec.
```rust
// single-line comment
/* multi-line
comment */
valid_identifier
_private
MyStruct
42 0x2A 0b101010 0o52
"a string" "\n \t \\ \""
true false
```
Semicolons are mandatory statement terminators in both languages.
### Reserved keywords (union of both languages)
```
as async await break class
const continue defer dyn else
enum false fn for if
impl include is let loop
match mut object priv pub
return self Self spawn static
struct trait true while
```
DSCX-only: `class`, `object`, `trait` (as a type-position bound), `async`, `await`,
`spawn`, `is`, `match` (no-scrutinee form). All are usable in DSCS only where noted below
(DSCS has its own, smaller `trait`).
---
## 3. DSCS: Types
### Primitives
```rust
i8 i16 i32 i64 isize
u8 u16 u32 u64 usize
bool
str // pointer to a length-prefixed byte sequence
void // absence of value, return-type only
```
### Pointers and ownership
```rust
&T // borrowed, immutable
&mut T // borrowed, mutable
Box<T> // owned, linear — see §4
Rc<T> // shared, refcounted, linear handle — see §4
```
### Arrays and generics
```rust
[u32; 10]
Vec<T>
Map<K, V>
```
### Algebraic types
```rust
enum Shape {
Circle(isize),
Rect(isize, isize),
Point,
}
```
Enums carry data per variant, matched exhaustively with `match` (§6). No implicit null —
absence is always `Option<T>` (an ordinary two-variant enum in the standard library).
---
## 4. DSCS: Ownership
DSCS has no borrow checker and no lifetimes. It adds exactly one piece of static
checking beyond plain C-like pointers: **linear ("used-once") tracking** for `Box<T>` and
`Rc<T>` handles.
- A `Box<T>` value must be consumed exactly once along every path out of its scope —
either freed explicitly (`.drop()`) or moved out (returned, stored, passed to a
function taking ownership).
- `Box<T>` is conceptually a fat pointer: `{ ptr: *T, alloc: &Allocator }`. `.drop()`
always frees through the allocator it was created with — the caller never needs to
remember or re-pass the allocator.
- `Rc<T>` follows the same linear-handle discipline. `.clone()` consumes a `&Rc<T>` and
produces a new, independent owned handle (bumping the refcount); `.drop()` decrements,
freeing at zero.
- Plain `&T` / `&mut T` are just borrows — no linearity tracking, no aliasing rules. The
programmer is responsible for not creating dangling references, exactly as in C.
```rust
fn make_point(alloc: &Allocator) -> Box<Point> {
Box::new(alloc, Point { x: 0, y: 0 })
}
fn peek(p: &Point) -> isize {
p.x // borrowed, no ownership implications
}
fn main() {
let alloc = Allocator::system();
let p = make_point(&alloc); // p: Box<Point>, owned
print(peek(&p));
p.drop(); // required — linear checker enforces this
}
```
This is the entire mechanism DSCX relies on to automate memory management across the
language boundary: ownership is legible from the type, not from a comment.
---
## 5. DSCS: Structs, Traits, Generics
### Structs
```rust
struct Point {
x: isize,
y: isize,
}
impl Point {
fn new(x: isize, y: isize) -> Point {
Point { x: x, y: y }
}
fn len(&self) -> isize {
sqrt(self.x * self.x + self.y * self.y)
}
}
```
Structs are value types by default (copied on assignment unless explicitly boxed).
### Traits — static dispatch only
```rust
trait AsUnit {
fn unit(&self) -> Self;
}
impl AsUnit for Point {
fn unit(&self) -> Self {
let len = self.len();
Point { x: self.x / len, y: self.y / len }
}
}
```
DSCS traits are monomorphized at compile time (like Rust generics with trait bounds).
**There is no `dyn`, no vtable, no runtime dispatch in DSCS itself.** Where kernel code
needs runtime-polymorphic behavior (e.g. driver interfaces), build it explicitly as a
struct of function pointers — the same pattern C kernels use:
```rust
struct FileOps {
read: fn(&File, &mut [u8]) -> usize,
write: fn(&File, &[u8]) -> usize,
}
```
This manual pattern is exactly what DSCX's `dyn Trait` compiles down to (§8) — DSCS
users can build it by hand for the same effect, with full visibility into the indirection.
### Generics
```rust
struct Vec<T> {
data: Box<[T]>,
len: usize,
}
impl<T> Vec<T> {
fn push(&mut self, item: T) { /* ... */ }
}
```
Simple, monomorphized generics — no higher-kinded types, no const generics (yet), no
specialization.
---
## 6. DSCS: Functions, Control Flow, Memory
```rust
fn add(a: i32, b: i32) -> i32 {
a + b // implicit return of trailing expression
}
```
### Control flow
```rust
if x < 10 { ... } else { ... }
while i < len { ... }
loop { ... } // infinite, break to exit
match shape {
Shape::Circle(r) => 3 * r * r,
Shape::Rect(w, h) => w * h,
Shape::Point => 0,
}
```
DSCS `match` requires a scrutinee and must be exhaustive over the enum's variants — no
`is`, no dynamic-dispatch matching (those are DSCX-only, see §11).
### Memory: allocator passing + `defer`
```rust
fn read_file(alloc: &Allocator, path: str) -> Box<[u8]> {
let buf = alloc.alloc(1024);
defer buf.drop(); // runs on every exit path
// ...
buf
}
```
`defer` schedules an expression to run at scope exit, in reverse declaration order,
regardless of which `return` path is taken — identical to the original DSC spec.
---
## 7. DSCS: Concurrency (Task/Poll)
DSA has no true parallelism; concurrency is cooperative and single-threaded. DSCS
exposes this as a plain trait, not language syntax:
```rust
enum Poll<T> { Ready(T), Pending }
trait Task {
type Output;
fn poll(&mut self) -> Poll<Self::Output>;
}
```
Only operations that can genuinely suspend implement `Task`. Ordinary DSCS code is
unaffected and stays synchronous.
```rust
struct UartRead;
impl Task for UartRead {
type Output = u8;
fn poll(&mut self) -> Poll<u8> {
if uart_has_byte() { Poll::Ready(uart_read_byte()) } else { Poll::Pending }
}
}
```
A kernel scheduler polls all live tasks each loop iteration, and idles the core with the
`hlt()` intrinsic when nothing made progress, resuming on the next interrupt:
```rust
fn run(tasks: &mut Vec<Box<dyn Task<Output = ()>>>) -> ! {
loop {
let mut progressed = false;
for t in tasks.iter_mut() {
if let Poll::Ready(()) = t.poll() { progressed = true; }
}
if !progressed { hlt(); }
}
}
```
ISRs never run task logic directly — they flip a flag or push to a queue that the
relevant `Task::poll()` checks (e.g. `UART_READY`). Shared state touched by both an ISR
and the scheduler must have interrupts masked during access, since a single core can
still be preempted mid-operation by an interrupt.
DSCX's `async`/`await`/coroutines (§13) compile down to hand-written-equivalent `Task`
state machines using exactly this vocabulary.
---
## 8. DSCX: Classes and Inheritance
```rust
class Point2 {
let x: isize,
let y: isize,
fn len(self) -> isize {
sqrt(self.x * self.x + self.y * self.y)
}
}
// single class inheritance + multiple trait conformance
class Point3: Point2 + AsUnit {
let z: isize,
fn len(self) -> isize {
sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
}
fn AsUnit.unit(self) -> Self {
let len = self.len();
Self { x: self.x / len, y: self.y / len, z: self.z / len }
}
}
```
- Classes are **reference types** by default (Kotlin-style) — unlike DSCS structs, which
are value types. `self`/`&self` collapse to the same meaning at the class level.
Actual by-value semantics belong to DSCS structs.
Method receivers use bare `self`.
- A class may inherit from **one** class and implement **any number** of traits
(`Class: Base + TraitA + TraitB`).
- Trait-method qualification (`fn AsUnit.unit(self)`) disambiguates a method name that
collides with an inherited or another trait's method; it's only required where a
collision actually exists.
- Memory is never manual here — DSCX auto-inserts alloc/free for provably single-owner
values, and falls back to refcounting when a value's escape analysis shows multiple
live aliases or a thread/coroutine boundary crossing. Both paths lower to explicit
`Box`/`Rc` + `defer` in the generated DSCS.
- `dyn Trait` (implicit for bare trait-typed parameters, e.g. `fn f(val: AsUnit)`)
compiles to the manual vtable-struct pattern from §5.
---
## 9. DSCX: Properties
```rust
class Circle(val r: i32) {
var area: i32
get() = 3 * r * r
set(value) { r = sqrt(value / 3) }
}
let c = Circle(5)
let a = c.area // desugars to c.get_area()
c.area = 20 // desugars to c.set_area(20)
```
Compound assignment through a property (`c.area += 1`) desugars to
`c.set_area(c.get_area() + 1)`, evaluating a non-trivial receiver expression exactly
once via a compiler-generated temporary.
---
## 10. DSCX: Trailing Lambdas
A bare `{ }` immediately following a call is a trailing lambda argument — never `|params|`
in that position (which would collide with bitwise-or):
```rust
items.map { i -> i + 1 }
items.map { it + 1 } // implicit single parameter `it`
```
Non-trailing closures still use `|params| expr` where no ambiguity exists:
```rust
let f = |i| i + 1;
```
Optional callback/lambda parameters are handled with **default parameter values**, not
`Option`, when the callee doesn't need to branch on the argument's presence:
```rust
fn sort_by(self, cmp: (T, T) -> i32 = natural_order) { ... }
list.sort_by() // uses default
list.sort_by { a, b -> a - b } // trailing lambda overrides default
```
`Option<Closure>` is reserved for cases where absence is itself meaningful data the
callee must `match` on.
**Grammar note:** any construct introduced by a bare keyword followed by `{` (`if`,
`while`, `match` with a scrutinee) disallows a bare top-level `{`-starting expression —
including a trailing-lambda call — directly in that position; wrap in parens if needed.
`match` itself (being a keyword, not an identifier) can never be mistaken for a
trailing-lambda call target, so `match { ... }` (no scrutinee, see §11) is unambiguous.
---
## 11. DSCX: Pattern Matching and `is`
### Ordinary exhaustive match (enums — identical to DSCS)
```rust
match shape {
Shape::Circle(r) => ...,
Shape::Rect(w, h) => ...,
Shape::Point => ...,
}
```
### `is` — hierarchy-aware type test
```rust
match val {
is Point3 => true, // true only for Point3 (no known subclasses)
is Point2 => true, // true for Point2 and any subclass, incl. Point3
is AsUnit => true, // true for any implementor
}
```
`dyn`-trait / class-hierarchy matches are **not** provably exhaustive, so a wildcard arm
is mandatory:
```rust
match val {
is Point3 => ...,
_ => ...,
}
```
Binding a narrowed value reuses ordinary `name: Type` syntax rather than a tuple-struct-
like `Type(name)` (which would collide with real tuple-struct patterns) or an `as`/`@`
suffix:
```rust
match val {
is p: Point3 => p.some_point3_only_method(), // p: Point3, explicitly bound
is Point2 => true,
}
```
No implicit flow-sensitive smart-casting — narrowing always requires the explicit
`is name: Type` bind, deliberately trading Kotlin's automatic narrowing for a much
smaller type-checker feature.
### No-scrutinee `match` — if/else-if chain, sugar
```rust
match {
do_some_processing() == 1 => println("success!"),
something_else() == 1 => println("success"),
val is Point3 => { return true; },
false => unreachable(),
}
```
Every arm's left-hand side is an ordinary `bool`-valued expression (`is` included, since
`is` is itself just a boolean-returning infix operator) — arms are evaluated top to
bottom, lazily, first match wins. **Commas between arms are mandatory**, even when an
arm's right-hand side is a `{ }` block, to avoid reintroducing whitespace-sensitive
parsing.
---
## 12. DSCX: Optionals
`Option<T>` — no null, ever. `T?` is pure sugar for `Option<T>` in **type position**
only:
```rust
let x: i32? = None; // sugar for Option<i32>
```
`?` as a postfix operator remains reserved for Rust-style error/`None` propagation in
**expression position**:
```rust
fn parse(s: str) -> Option<i32> {
let n = try_parse(s)?; // propagates None early
Some(n)
}
```
No Kotlin-style `?.` safe-call or `!!` non-null assertion — those don't exist since
there's no null to guard against.
---
## 13. DSCX: Objects and Coroutines
### Singletons via `object`
```rust
object Logger {
var count: i32 = 0
fn log(msg: String) {
count += 1
println(msg)
}
}
Logger.log("started")
```
Lowers to a lazily-initialized static DSCS struct instance plus accessor functions —
the object itself is not a class instance the user constructs.
### Coroutines
```rust
async fn read_line() -> String {
let b = uart.read_byte().await;
// ...
}
let handle = spawn { heavy_computation() }
let result = handle.await()
```
- **Explicit `await`** at every suspension point (not Kotlin's implicit `suspend fun`) —
this is what makes the transpile to DSCS mechanical: every `.await` is a known split
point for the state-machine transform.
- The compiler lowers an `async fn` into a generated struct (holding whatever locals
must survive across suspension points) implementing DSCS's `Task` trait (§7), with
`poll()` dispatching via a generated resume-point jump table — the same shape as
Rust's async transform, targeting DSCS's plain trait instead of a compiler-magic type.
- `spawn { }` hands a block to the runtime's scheduler as a new `Task`; `.await()` on the
returned handle polls until ready.
---
## 14. Example Programs
### 14.1 DSCS — pointer-heavy: a manual vector
```rust
struct Vec<T> {
data: Box<[T]>,
len: usize,
}
impl<T> Vec<T> {
fn new(alloc: &Allocator, capacity: usize) -> Vec<T> {
Vec { data: Box::new_slice(alloc, capacity), len: 0 }
}
fn push(&mut self, item: T) {
if self.len == self.data.len() {
self.grow();
}
self.data[self.len] = item;
self.len += 1;
}
fn get(&self, i: usize) -> &T {
&self.data[i]
}
fn drop(self) {
self.data.drop();
}
}
fn main() {
let alloc = Allocator::system();
let mut v: Vec<i32> = Vec::new(&alloc, 4);
defer v.drop();
v.push(1);
v.push(2);
let mut i: usize = 0;
while i < v.len {
print(*v.get(i));
i += 1;
}
}
```
### 14.2 DSCS — kernel: UART driver as a Task
```rust
static UART_READY: bool = false;
extern "isr" fn uart_isr() {
UART_READY = true;
}
struct UartRead;
impl Task for UartRead {
type Output = u8;
fn poll(&mut self) -> Poll<u8> {
if UART_READY {
UART_READY = false;
Poll::Ready(uart_read_byte())
} else {
Poll::Pending
}
}
}
fn kernel_main() -> ! {
let mut tasks: Vec<Box<dyn Task<Output = ()>>> = Vec::new(&Allocator::kernel(), 8);
// register drivers as tasks...
loop {
let mut progressed = false;
for t in tasks.iter_mut() {
if let Poll::Ready(()) = t.poll() { progressed = true; }
}
if !progressed { hlt(); }
}
}
```
### 14.3 DSCX — application UI (from the working draft)
```rust
/* uilib.dscx */
class UiBuilder {
fn text_entry(field: String) -> Interaction
fn button(label: str) -> Interaction
fn vertical(body: () -> void)
}
class Interaction {
fn clicked() -> bool
}
/* app.dscx */
pub enum Action {
SubmitForm(PasswordForm)
}
class PasswordForm {
let username: String,
let password: String,
}
class UserInterface {
const MESSAGE: str = "Run Emulator"
let form: PasswordForm
let state: EmulatorState
fn new(state: EmulatorState) -> Self {
Self {
form: PasswordForm { username: String::new(), password: String::new() },
state,
}
}
fn ui(self, ui: UiBuilder) -> Action {
ui_lib::Form(self.form, ui, "login") { form, ui ->
ui.text_entry(form.username) { view -> view.placeholder = "enter username" }
ui.text_entry(form.password) // placeholder has a default value
if ui.button("Submit").clicked() {
return Action::SubmitForm(form)
}
}
ui.vertical {
my_lib::StatusIndicator(self.state.status, ui)
if ui.button("Run Emulator").clicked() {
self.state.run()
}
}
}
}
```
### 14.4 DSCX — coroutine-based file read
```rust
async fn read_config(path: String) -> Result<Config, IoError> {
let bytes = fs::read(path).await?
Config::parse(bytes)
}
fn main() {
let handle = spawn { read_config("app.toml") }
match handle.await() {
Ok(cfg) => println("loaded: ${cfg.name}"),
Err(e) => println("failed: ${e}"),
}
}
```
---
## 15. Grammar Summary (partial — DSCX additions over DSCS)
```ebnf
(* DSCX no-scrutinee match *)
match_expr = "match" [ scrutinee ] "{" match_arm { "," match_arm } [ "," ] "}"
match_arm = pattern_or_bool "=>" expression
pattern_or_bool = bool_expr
| "is" [ identifier ":" ] type_path
| "_"
(* DSCX is-test as a standalone boolean expression *)
is_expr = expression "is" [ identifier ":" ] type_path
(* trailing lambda *)
trailing_lambda = call_expr "{" [ lambda_params "->" ] { statement } "}"
lambda_params = identifier { "," identifier }
(* class definition *)
class_def = "class" identifier [ ":" inherit_list ] "{" { class_member } "}"
inherit_list = type_path { "+" type_path }
(* property *)
property_decl = ("val" | "var") identifier ":" type
[ "get" "(" ")" ( "=" expression | block ) ]
[ "set" "(" identifier ")" block ]
(* optional sugar *)
optional_type = type "?" (* == Option<type> *)
(* ownership-bearing DSCS types *)
owned_type = "Box" "<" type ">"
shared_type = "Rc" "<" type ">"
borrowed_type = "&" [ "mut" ] type
```
---
## Appendix A: DSCS vs DSCX vs Rust
| Feature | DSCS | DSCX | Rust |
|---|---|---|---|
| Encapsulation | `struct` + `impl` | `class` (ref type) | `struct`/`enum` + `impl` |
| Default aggregate semantics | value type | reference type | value type (move) |
| Inheritance | none | single class + multi-trait | none (trait composition only) |
| Trait dispatch | static/monomorphized only | static or `dyn` (manual-vtable lowering) | static or `dyn` |
| Memory management | manual (`Box`, `defer`, explicit allocator) | automatic (inferred drop / refcount) | borrow-checked, compile-time |
| Borrow checking | none — linear `Box`/`Rc` tracking only | none (inherited from DSCS output) | full borrow checker + lifetimes |
| Pattern matching | exhaustive `match` over enums | `match` (enum) + `is` (hierarchy, non-exhaustive) | exhaustive `match` |
| Optionals | `Option<T>` | `Option<T>` / `T?` sugar | `Option<T>` |
| Concurrency | `Task`/`Poll` trait, hand-written state machines | `async`/`await`/`spawn`, compiler-generated state machines | `Future`/`async`/`await` |
| Closures | none (function pointers only) | trailing lambdas, `|params| expr` | closures, `Fn`/`FnMut`/`FnOnce` |
| Singletons | manual `static` | `object` keyword | no direct equivalent (`static`/`lazy_static`) |
---
*End of DSCS & DSCX Language Specification Draft v0.1*
+253
View File
@@ -0,0 +1,253 @@
# 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<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;
```
View File
+633
View File
@@ -0,0 +1,633 @@
# DSCS Reference (Draft v0.3)
**DSCS — Damn Simple Code (Systems)**
## Introduction
DSCS is the low-level half of the DSC language family — a simple, explicit systems
language with the capabilities of a language like C, but with improved safety and
ergonomics. It compiles down to DSA assembly and is meant to be usable directly for
things like kernels and drivers.
DSCS is also the foundation DSCX (the higher-level, OOP-flavoured member of the
family) transpiles down to: every valid DSCS program is a valid DSCX program. This
document covers DSCS on its own.
### Design goals
- A simple, explicit systems language with all the capabilities of a language like C,
with improved safety and ergonomics.
- **Static dispatch only.** Generics are monomorphized at compile time; there is no
built-in `dyn Trait`, no vtables, and no hidden indirect calls. Where genuine
runtime polymorphism is needed, you build it explicitly — see
[Runtime Polymorphism](#runtime-polymorphism-without-dynamic-dispatch).
- **Manual memory management.** No garbage collector, no automatic reference
counting. Allocators are passed explicitly wherever allocation happens.
- **No borrow checker, no lifetimes.** DSCS trusts the programmer. See
[Ownership & Memory Safety](#ownership--memory-safety).
---
## Lexical Structure
```rust
// single line comment...
/* multi
line
comment */
```
---
## Variables, Constants & Statics
```rust
fn main() {
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.
```
---
## Primitive Types & Literals
```rust
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;
}
```
### Arrays
Because the size is stored with the type, the compiler can infer the length:
```rust
let arr: [u32; 6] = [0, 1, 2, 3, 4, 5];
// = [1; 6]; // alternative syntax for [1, 1, 1, 1, 1, 1]
```
### Pointers
```rust
// 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;
```
`*T` is read-only; `*mut T` is read/write. Full rules governing when a pointer is
safe to use are covered in [Ownership & Memory Safety](#ownership--memory-safety) —
raw pointers carry no compiler-enforced validity guarantee at all.
---
## Control Flow
`if`/`else` are expressions — the same "no trailing semicolon on the last line is the
return value" rule applies as in function bodies:
```rust
let sign = if x > 0 { 1 } else if x < 0 { -1 } else { 0 };
```
For a single-statement body, `=>` can be used instead of braces, aligning with
`match`'s own arm syntax:
```rust
if condition => do_y();
```
The same rule applies to `else`, and the two forms compose — a branch can use
whichever form fits it:
```rust
if condition => do_y(); else { a(); b(); }
```
As an expression, the arrow form has no trailing semicolon on either branch — `else`
(a keyword) is what tells the parser where a branch's expression ends, same as any
other expression grammar stopping at a token it can't extend through:
```rust
let sign = if x > 0 => 1 else if x < 0 => -1 else => 0;
```
**Dangling-else rule:** when `if` statements nest without an explicit `else` on the
inner one, `else` binds to the *nearest* unmatched `if` — the same convention C and
C++ use. This is a well-known ambiguity class (it's the specific reason Rust
mandates braces on `if` bodies), reintroduced here deliberately in exchange for the
more compact single-statement form; use explicit braces on the inner `if` whenever
the binding could be unclear:
```rust
if a => if b => x(); else => y(); // else binds to `if b`, not `if a`
if a { if b => x(); } else => y(); // braces make the outer binding explicit instead
```
`while` loops:
```rust
let mut i: u32 = 0;
while i < 10 {
i += 1;
}
```
There's no `for` loop construct defined yet — worth deciding later whether it's
built as sugar over `while` or added as its own construct.
---
## Structs
```rust
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 Point::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),
}
}
}
```
---
## Enums
You can use enums to represent various different types at the same time — useful for
state machines.
```rust
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,
}
}
}
```
---
## Generics
DSCS supports simple generics with static dispatch — every generic function or
struct is monomorphized once per concrete type it's used with at compile time. There
is no runtime type erasure and no hidden indirection.
```rust
struct Vec<T> {
field data: *mut T;
field capacity: u32;
field length: u32;
// with_capacity takes a generic T and an allocator, and returns an instance of
// Self holding 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<A: Allocator>(alloc: *mut A, 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 A, new_size: u32) {
self.data = alloc.realloc(self.data as *mut void, new_size) as *mut T;
self.capacity = new_size;
}
// consumes self and frees its memory.
fn drop(self, alloc: *mut A) {
alloc.free(self.data as *mut *void);
}
}
```
> **Note on the original draft:** the earlier version of this example passed
> `alloc: *mut Allocator` directly, with `Allocator` used as a bare trait type rather
> than an explicit generic bound. This is intentional, not an oversight — see
> [Bare Trait Names as Types](#bare-trait-names-as-types) below for the rule.
---
## Traits
```rust
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);
// reallocates the block at `ptr` to be `new_size` bytes, and returns a pointer
// to the (possibly moved) block.
fn realloc(*mut self, ptr: *mut void, new_size: u32) -> *mut void;
}
trait DivisibleByN<T: Integer> {
// the integer_div method belongs to the Integer trait.
fn is_divisible(*self, n: u32) -> bool {
self.integer_div(n) == 0
}
}
```
Because traits are always statically dispatched, a bound like `<T: Integer>` or
`<A: Allocator>` is resolved and monomorphized entirely at compile time — calling a
trait method never involves a runtime lookup.
### Bare Trait Names as Types
Since DSCS has no dynamic dispatch, a bare trait name used directly in a type
position is unambiguous — there's no competing `dyn Trait` reading it could be
confused with — so it's allowed as shorthand for an implicit static-dispatch generic
parameter, the same idea as Rust's `impl Trait` in argument position, just without
the `impl` keyword:
```rust
fn alloc_point(a: *mut Allocator) -> Box<Point> { ... }
// exactly equivalent to:
fn alloc_point<A: Allocator>(a: *mut A) -> Box<Point> { ... }
```
**Each occurrence gets its own independent, anonymous type parameter** — this
matches Rust's actual `impl Trait` semantics exactly, including the consequence that
two bare-trait-typed parameters in the same signature are *not* required to be the
same concrete type:
```rust
fn transfer(src: *mut Allocator, dst: *mut Allocator) { ... }
// src and dst may be DIFFERENT concrete Allocator types — nothing here forces them
// to agree.
```
If two or more parameters need to share the same concrete type, use an explicit
named generic bound instead:
```rust
fn transfer<A: Allocator>(src: *mut A, dst: *mut A) { ... }
// src and dst are now forced to be the same concrete allocator type.
```
This sugar is currently defined for parameter position only. Return-position bare
trait names (Rust's `impl Trait` return-position equivalent) raise a separate
question — an opaque return type's exact size wouldn't be visible to the caller,
which matters more in a systems language than it does in Rust — and aren't covered
by this rule yet.
---
## Function Types
Functions are values too — this matters in systems programming, since interrupt
handlers need to be installed as data the machine can jump to:
```rust
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 `Func<(ArgTypes...), RetType>`, with the extra
// hardcoded constraint that the argument list must be written as a tuple type, even
// for a single argument.
let handler: Func<(), void> = invalid_instruction_fault;
let func: Func<(u32, u32), u32> = add;
func(1, 2); // == 3
```
A `Func<...>` value is nothing more than the address of the function it points to —
one machine word, `Copy`, no heap allocation. It **cannot** capture or close over
local variables; it's purely a code address. If you need a callable value that also
carries its own state, see the next section.
---
## Runtime Polymorphism (without dynamic dispatch)
Sometimes you genuinely need runtime polymorphism — for example, a driver table
where the concrete device type isn't known until the kernel is running. DSCS has no
built-in `dyn Trait` for this. Instead, you build the vtable yourself, as an ordinary
struct of function pointers:
```rust
struct ShapeOps {
field area: Func<(*void), u32>;
}
fn circle_area(self: *void) -> u32 {
let c = self as *Circle;
3 * (*c).radius * (*c).radius
}
const CIRCLE_OPS: ShapeOps = ShapeOps { area: circle_area };
// A caller holding a `*void` payload pointer and a `ShapeOps` table can now call
// `(ops.area)(payload)` without ever knowing the concrete type behind the pointer —
// this is exactly what `dyn Trait` does in other languages, just fully visible and
// under your control instead of compiler-generated.
```
This is also the pattern to reach for whenever you need something closure-shaped: a
`Func<...>` paired with its own `*void` (or concretely-typed) state pointer, bundled
into a small struct.
---
## Memory Management
You have a few different ways to manage memory.
### Manual alloc/free (discouraged)
```rust
fn main() {
let mut alloc = ArenaAllocator::new(HEAP_START);
// 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 as *mut *void); // don't use pointer now!
}
```
### `defer`
Deferring execution of a statement means it's executed as soon as the enclosing
function returns — regardless of which return path was taken. Multiple `defer`
statements in the same function run in reverse order (last deferred, first executed),
the same as Zig.
### `Box<T>`
```rust
fn main() {
let mut point = Box::new(alloc, Point::new(2, 3));
// Box::drop(self, alloc) uses the passed allocator to free its contents when called.
defer point.drop(alloc);
// Box is implemented as a transparent type so we can simply use Box<T> as if it were a *mut T!
// method calls on a Box<T> auto-dereference through it exactly as they would
// through a raw *mut T receiver.
point.set_y(5);
}
```
`Box<T>` has exactly one owner. Cloning a `Box<T>` is a **deep** clone — it allocates
fresh memory and copies the underlying value, which is why cloning a `Box` requires
passing an allocator explicitly, same as any other allocation.
### `Rc<T>`
`Rc<T>` is for values that need multiple simultaneous owners. Unlike `Box<T>`,
cloning an `Rc<T>` is cheap: it doesn't touch or copy the underlying data at all, it
just increments a reference count and hands back a new, independent `Rc<T>` handle to
the *same* memory.
```rust
fn main() {
let a = Rc::new(alloc, Point::new(1, 2));
let b = a.clone(); // cheap: bumps the refcount, no allocation, no data copy
defer a.drop(); // decrements the refcount
defer b.drop(); // decrements the refcount; frees the Point once it reaches zero
}
```
Like `Box<T>`, each individual `Rc<T>` *handle* is still linear — once you've dropped
or moved a given handle, that specific handle is gone and must not be used again.
Nothing stops you from calling `.clone()` first, though, to mint another independent
handle before doing so.
---
## Ownership & Memory Safety
DSCS's ownership model is loosely inspired by Rust's, with one major simplification:
**there are no references, only pointers.** `*T`/`*mut T` are not tracked or
validity-checked by the compiler at all.
The rules that *do* apply:
- Passing a value **by value** (e.g. `fn unit(self) -> Self`, not `*self`) moves it
into the function. After the call, the caller's copy is gone — any pointers still
pointing at it may now be dangling and must not be used.
- Once a value has been dropped (via `defer`, an explicit `.drop()` call, or a raw
`alloc.free(...)`) or moved out of its original binding, **you must not create new
pointers to it**, and any pointers created before that point are no longer valid to
dereference.
- There is **no borrow checker and no lifetimes**. The compiler does not verify any
of the above for you — it trusts the programmer entirely. This is a deliberate
simplicity/performance tradeoff relative to Rust: DSCX, the higher-level language
built on top of DSCS, is where automatic safety is intended to live.
```rust
fn dangling_example(alloc: *mut Allocator) -> *u32 {
let boxed = Box::new(alloc, 5);
let p: *u32 = &*boxed; // p now points at boxed's memory
boxed.drop(alloc); // the memory is freed here
p // DANGLING: p now points at freed memory. DSCS will not stop you from
// returning or dereferencing it — this is undefined behaviour.
}
```
---
## Error Handling
Like Rust, error handling is done with `Result<T, E>` and optionals with `Option<T>`.
```rust
fn parse_digit(c: u8) -> Result<u32, ParseError> {
if c >= '0' && c <= '9' {
Result::Ok((c - '0') as u32)
} else {
Result::Err(ParseError::NotADigit)
}
}
fn find(arr: [u32; 6], target: u32) -> Option<u32> {
let mut i: u32 = 0;
while i < 6 {
if arr[i] == target {
return Option::Some(i);
}
i += 1;
}
Option::None
}
```
An **owned** value in DSCS can never be null or none. Only a pointer can be null, and
only once it's been explicitly cast:
```rust
let p: *u32 = *null; // must be explicit; there is no implicit null for owned values
```
---
## Concurrency
DSCS has no threads and no language-level `async`/`await`. Concurrency is
cooperative and single-threaded, built from two small pieces — `Poll<T>` and `Task`
are ordinary library-level types, not compiler magic:
```rust
enum Poll<T> {
variant Ready(T);
variant Pending;
}
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 }
}
}
```
A scheduler is ordinary DSCS code that polls a task on a loop, halting the CPU (via
the `hlt` intrinsic — real hardware idling, not a busy spin) whenever there's nothing
left to do:
```rust
fn run_scheduler(mut task: Keyboard) {
loop {
match task.poll() {
Poll::Ready(byte) => { /* handle it, then reschedule or exit */ }
Poll::Pending => hlt(), // idle until the next interrupt wakes us
}
}
}
```
A scheduler juggling several different concrete task types at once needs the same
manual-vtable pattern from [Runtime Polymorphism](#runtime-polymorphism-without-dynamic-dispatch)
— a struct pairing a `Func<(*void), Poll<...>>` with its own state pointer — since
DSCS has no `dyn Trait` to erase each task's concrete type automatically.
### Interrupt Service Routines
An ISR should do as little as possible — typically just flip a flag that the
corresponding `Task`'s `poll()` checks on its next pass. It must never run task logic
directly:
```rust
static UART_READY: bool = false;
fn uart_isr() {
UART_READY = true;
}
struct UartRead;
impl Task for UartRead {
type Output = u8;
fn poll(*mut self) -> Poll<u8> {
if UART_READY {
UART_READY = false;
Poll::Ready(uart_read_byte())
} else {
Poll::Pending
}
}
}
```
Because an ISR can preempt the scheduler at any instruction boundary — even on a
single-core machine like DSA — any state touched by both an ISR and ordinary code
(like `UART_READY` above) needs interrupts masked around the access if it isn't a
single atomic instruction, to avoid a torn read/write.