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
- Introduction
- Lexical Structure
- DSCS: Types
- DSCS: Ownership
- DSCS: Structs, Traits, Generics
- DSCS: Functions, Control Flow, Memory
- DSCS: Concurrency (Task/Poll)
- DSCX: Classes and Inheritance
- DSCX: Properties
- DSCX: Trailing Lambdas
- DSCX: Pattern Matching and
is - DSCX: Optionals
- DSCX: Objects (Singletons) and Coroutines
- Example Programs
- Grammar Summary (partial)
- 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
.dscsfile is always a legal.dscxfile. 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 Tare 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/&selfcollapse to the same meaning at the class level. Actual by-value semantics belong to DSCS structs. Method receivers use bareself. - 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+deferin 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
awaitat every suspension point (not Kotlin's implicitsuspend fun) — this is what makes the transpile to DSCS mechanical: every.awaitis a known split point for the state-machine transform. - The compiler lowers an
async fninto a generated struct (holding whatever locals must survive across suspension points) implementing DSCS'sTasktrait (§7), withpoll()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 newTask;.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