documentation updates
This commit is contained in:
Generated
+2
@@ -20,6 +20,8 @@
|
||||
<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/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$/doc/.obsidian" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
|
||||
@@ -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*
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user