Files
DSA/doc/notes/dscs-dscx-language-spec.md
2026-08-20 22:22:09 +01:00

22 KiB

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
  2. Lexical Structure
  3. DSCS: Types
  4. DSCS: Ownership
  5. DSCS: Structs, Traits, Generics
  6. DSCS: Functions, Control Flow, Memory
  7. DSCS: Concurrency (Task/Poll)
  8. DSCX: Classes and Inheritance
  9. DSCX: Properties
  10. DSCX: Trailing Lambdas
  11. DSCX: Pattern Matching and is
  12. DSCX: Optionals
  13. DSCX: Objects (Singletons) and Coroutines
  14. Example Programs
  15. Grammar Summary (partial)
  16. 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).


2. Lexical Structure

Comments, identifiers, and literals are unchanged from the original DSC spec.

// 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

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

&T          // borrowed, immutable
&mut T      // borrowed, mutable
Box<T>      // owned, linear — see §4
Rc<T>       // shared, refcounted, linear handle — see §4

Arrays and generics

[u32; 10]
Vec<T>
Map<K, V>

Algebraic types

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.
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

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

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:

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

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

fn add(a: i32, b: i32) -> i32 {
    a + b   // implicit return of trailing expression
}

Control flow

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

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:

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.

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:

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

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

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):

items.map { i -> i + 1 }
items.map { it + 1 }              // implicit single parameter `it`

Non-trailing closures still use |params| expr where no ambiguity exists:

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:

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)

match shape {
    Shape::Circle(r) => ...,
    Shape::Rect(w, h) => ...,
    Shape::Point => ...,
}

is — hierarchy-aware type test

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:

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:

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

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:

let x: i32? = None;          // sugar for Option<i32>

? as a postfix operator remains reserved for Rust-style error/None propagation in expression position:

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

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

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

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

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)

/* 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

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)

(* 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
Singletons manual static object keyword no direct equivalent (static/lazy_static)

End of DSCS & DSCX Language Specification Draft v0.1