diff --git a/.idea/dsa2.iml b/.idea/dsa2.iml index 3f76210..5b6d67e 100644 --- a/.idea/dsa2.iml +++ b/.idea/dsa2.iml @@ -20,6 +20,8 @@ + + diff --git a/doc/DSC/dscx.md b/doc/DSC/dscx.md deleted file mode 100644 index e69de29..0000000 diff --git a/doc/DSA State Machine Semantics.md b/doc/notes/DSA State Machine Semantics.md similarity index 100% rename from doc/DSA State Machine Semantics.md rename to doc/notes/DSA State Machine Semantics.md diff --git a/doc/dsc.md b/doc/notes/dsc.md similarity index 100% rename from doc/dsc.md rename to doc/notes/dsc.md diff --git a/doc/notes/dscs-dscx-language-spec.md b/doc/notes/dscs-dscx-language-spec.md new file mode 100644 index 0000000..4abe1f4 --- /dev/null +++ b/doc/notes/dscs-dscx-language-spec.md @@ -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` (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 // owned, linear — see §4 +Rc // shared, refcounted, linear handle — see §4 +``` + +### Arrays and generics + +```rust +[u32; 10] +Vec +Map +``` + +### 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` (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` and +`Rc` handles. + +- A `Box` 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` 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` follows the same linear-handle discipline. `.clone()` consumes a `&Rc` 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 { + 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, 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 { + data: Box<[T]>, + len: usize, +} + +impl Vec { + 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 { Ready(T), Pending } + +trait Task { + type Output; + fn poll(&mut self) -> Poll; +} +``` + +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 { + 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>>) -> ! { + 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` 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` — no null, ever. `T?` is pure sugar for `Option` in **type position** +only: + +```rust +let x: i32? = None; // sugar for Option +``` + +`?` as a postfix operator remains reserved for Rust-style error/`None` propagation in +**expression position**: + +```rust +fn parse(s: str) -> Option { + 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 { + data: Box<[T]>, + len: usize, +} + +impl Vec { + fn new(alloc: &Allocator, capacity: usize) -> Vec { + 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 = 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 { + if UART_READY { + UART_READY = false; + Poll::Ready(uart_read_byte()) + } else { + Poll::Pending + } + } +} + +fn kernel_main() -> ! { + let mut tasks: Vec>> = 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 { + 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 *) + +(* 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` | `Option` / `T?` sugar | `Option` | +| 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* diff --git a/doc/notes/dscs_reference_draft.md b/doc/notes/dscs_reference_draft.md new file mode 100644 index 0000000..874b2cb --- /dev/null +++ b/doc/notes/dscs_reference_draft.md @@ -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 { + field data: *mut T; + field capacity: u32; + field length: u32; + + // with_capacity takes a generic T and returns an instance of Self with that type T. + // DSCS is a language with manual memory management, so you must pass an allocator whenever you intend to + // allocate dynamic memory. + fn with_capacity(alloc: *mut Allocator, capacity: u32) -> Self { + let ptr = alloc.alloc(capacity) as *mut T; + Self { + data: ptr, + capacity, + length: 0 + } + } + + fn length(*self) -> u32 { + self.length + } + + fn resize(*mut self, alloc: *mut Allocator, new_size: u32) { + self.data = alloc.realloc(self.data, u32); + } + + // consumes self and frees its memory. + fn drop(self, alloc: *mut Allocator) { + alloc.free(self.data); + } +} + +// in the above example, allocator is also a statically dispatched opaque type. +// the Allocator given could essentially be anything. all we know as the user is that the allocator implements the Allocator trait: + +// traits + +trait Allocator { + // allocates a new block of memory of the given size and returns a pointer to it. + // a void pointer must be explicitly cast by the caller to the type that they intend to use it as, before it can be dereferenced. + // dereferencing a void pointer is a compiler error. + fn alloc(*mut self, size: u32) -> *mut void; + + // frees the memory allocated to `ptr` + // this also sets the pointer to a null pointer so that the user can check it has been freed. + // while we cannot set it to zero by dereferencing it, + // we can cast it to a *null which implicitly sets the pointer to zero. + fn free(*mut self, ptr: *mut *void); + + // this will probably also move the memory at that pointer to the new location. + fn realloc(*mut self, ptr: *mut void); +} + +trait DivisibleByN { + fn is_divisible(*self, n: u32) { + // the integer_div method belongs to the Integer trait. + self.integer_div(n) == 0; + } +} + +// memory management +fn main() { + // we have a few different ways to manage memory. + let mut alloc = ArenaAllocator::new(HEAP_START); + + // method 1 + // the pattern below is commonly used in C, however it's strongly discouraged in this language! + // the data is not a valid instance of SomeType, and is not guaranteed to be zero initialised. + // please don't do this, it makes it very easy for UB to happen! + let ptr = alloc.alloc(Point::size()) as *mut Point; + ptr.id = 1; + + alloc.free(ptr); // don't use pointer now! + + // method 2: put it in a box! + let mut point = Box::new(Point::new(2, 3)); + // Box::drop(self, alloc) uses the passed allocator to free its contents when called. + // deferring execution of a function means it is executed as soon as the enclosing function returns. + defer ptr.drop(alloc); + + + // Box is implemented as a transparent type so we can simply use Box as if it were a *mut T! + point.set_y(5); +} + +// more traits +trait Task { + type Output; + fn poll(*mut self) -> Poll; +} + +struct Keyboard; +impl Task for Keyboard { + type Output = u8; + fn poll(*mut self) -> Poll { + if KEYBOARD.has_byte() { Poll::Ready(KEYBOARD.read()) } else { Poll::Pending } + } +} + +// function types! +fn add(x: u32, y: u32) -> u32 { + return x + y; +} + + +// in systems level programming we sometimes need to write ISRs to handle interrupts. +fn invalid_instruction_fault() { + // handle the interrupt +} +// but we can't just put a function in our code and expect interrupts to go to it. +// the machine needs to know where to find our interrupt handlers. + +// this means functions also need to be a type! +// C got around this using void pointers, but we can do better here. + +// a function pointer has the type signature `Func` with the extra hardcoded constraint that A must be a tuple of values. +let handler: Func<(), void> = invalid_instruction_fault; +let func: Func<(y32, u32), u32> = add; + + + + +// like rust, error handling is done with Result and optionals with Option. +// an owned value in DSCS cannot be a null or none type, however a pointer can be, if explicitly cast to *null; +``` + + + + + + + + + + + + diff --git a/doc/DSC/dscx.txt b/doc/notes/dscx.txt similarity index 100% rename from doc/DSC/dscx.txt rename to doc/notes/dscx.txt diff --git a/doc/dsi.md b/doc/notes/dsi.md similarity index 100% rename from doc/dsi.md rename to doc/notes/dsi.md diff --git a/doc/interrupts.md b/doc/notes/interrupts.md similarity index 100% rename from doc/interrupts.md rename to doc/notes/interrupts.md diff --git a/doc/reference/dscs-reference.md b/doc/reference/dscs-reference.md new file mode 100644 index 0000000..faefcd9 --- /dev/null +++ b/doc/reference/dscs-reference.md @@ -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 { + 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(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 { + // 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 `` or +`` 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 { ... } +// exactly equivalent to: +fn alloc_point(a: *mut A) -> Box { ... } +``` + +**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(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` + +```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 as if it were a *mut T! + // method calls on a Box auto-dereference through it exactly as they would + // through a raw *mut T receiver. + point.set_y(5); +} +``` + +`Box` has exactly one owner. Cloning a `Box` 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` + +`Rc` is for values that need multiple simultaneous owners. Unlike `Box`, +cloning an `Rc` 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` 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`, each individual `Rc` *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` and optionals with `Option`. + +```rust +fn parse_digit(c: u8) -> Result { + if c >= '0' && c <= '9' { + Result::Ok((c - '0') as u32) + } else { + Result::Err(ParseError::NotADigit) + } +} + +fn find(arr: [u32; 6], target: u32) -> Option { + 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` and `Task` +are ordinary library-level types, not compiler magic: + +```rust +enum Poll { + variant Ready(T); + variant Pending; +} + +trait Task { + type Output; + fn poll(*mut self) -> Poll; +} + +struct Keyboard; +impl Task for Keyboard { + type Output = u8; + fn poll(*mut self) -> Poll { + if KEYBOARD.has_byte() { Poll::Ready(KEYBOARD.read()) } else { Poll::Pending } + } +} +``` + +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 { + 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.