Why I’m building Dia
Dia stands for “do it all”. I’m building a language for writing complete applications, with a UI library, native compilation, and access to existing C libraries.
I want more of the work that goes into an application to be supported by the language and its standard libraries. Building an interface, editing rich text, calling a native library, and writing GPU code should fit into the same project without requiring a different language for each part.
The design draws from tools I like: Rust’s approach to ownership, JSX’s syntax for interfaces, Solid’s fine-grained reactivity, and ProseMirror’s approach to rich text editing. I’m interested in how these ideas work together, and how much application code they can save.
Dia is pre-alpha. This post describes the direction of the project; some features are implemented, and others are still being built.
What would you build with it?
A familiar way to write interfaces
If you write web applications, much of Dia’s UI code should look familiar. Components are functions, properties are typed parameters, and state changes update the parts of the interface that depend on them.
DSX: markup with Dia expressions
Dia has JSX-like syntax called DSX. The compiler checks component names, properties, and children along with the rest of your code. A misspelled property is a type error.
import ui.{View, run, Column, Text, Button, state}
struct CounterState {
value: i32
}
fn Counter(): View !{alloc} {
let s = state(CounterState{ value: 0 })
<Column spacing=12>
<Text>"Count: ${s.value}"</Text>
<Button
label="Increment"
on_click=() => { s.value += 1 }
/>
</Column>
}
pub fn main(): void !{alloc, io} {
run(() => <Counter />)
}
!{alloc} and !{alloc, io} declare allocation and I/O.Properties accept Dia expressions directly, so the callback needs no extra braces around it. Children are expressions too: strings, components, conditionals, and loops use the language’s ordinary syntax.
Updates follow the state you read
Dia’s reactivity design borrows heavily from Ryan Carniato’s work on Solid. Reading reactive state establishes a dependency. When that state changes, the computations and UI bindings that depend on it update. In the counter, changing s.value updates the count text.
let cart = state(Cart{ quantity: 2, unit_price: 15 })
<Text>"Total: ${cart.quantity * cart.unit_price}"</Text>
<Button label="Add one" on_click=() => { cart.quantity += 1 } />
Building reactivity alongside the type system gives Dia more information about how state moves through a program. For example, it can distinguish a tracked value from a plain copy, and reject a copy where a component requires ongoing updates.
See changes in a running application
I want the quick feedback of web development in a compiled native application. Incremental compilation and state-preserving hot reload are part of the compiler’s design, so changing a view can let you continue from the screen you were already testing.
// The running counter is at 7. Edit its view:
<Text>"Count: ${s.value}"</Text>
// Save this version:
<Text>"You clicked ${s.value} times"</Text>
// Intended result after reload: "You clicked 7 times"
Lazy imports and production code splitting are planned too. In a browser build, a settings screen should be able to load when someone opens it. Native builds can resolve the same import from code already linked into the application.
let settings = lazy import app.settings
settings.open()
Typed queries for the backend
The same attention to embedded syntax extends to the backend. The language design includes a typed SQL DSL, with the aim of checking queries and their result types as part of compilation. I want database code to get the same useful errors as interface code.
let min_age = 18
let users = sql {
select id, name from users where age >= ${min_age}
}
for user in users {
print(user.name)
// user.email: error, email was not selected
}
A UI library you can customize
Dia’s UI library uses its own GPU renderer, including for text. It does not require a webview. The aim is to share application and interface code across desktop, mobile, and browser targets.
That includes ordinary application work: forms, navigation, selection, keyboard input, and editable text. Those details take time to implement well. I want the standard UI library to handle them and make its underlying services available when an application needs something custom.
Reuse text editing in a custom canvas
Consider an infinite canvas with editable labels. You need text layout, caret movement, selection, and input handling inside a view that you draw yourself. Dia’s UI architecture is intended to let you reuse those text services in your own renderer.
<Canvas draw=(ctx) => {
ctx.circle(center=[120, 80], radius=40)
}>
<TextInput
@bind=label.text
position=[80, 140]
/>
</Canvas>
Keep the button, replace its drawing
The same principle applies to controls. I want you to be able to replace a button’s drawing with custom 2D shapes or a 3D effect while retaining its interaction behavior, accessibility semantics, and reactive properties.
// Standard drawing.
<Button label="Save" on_click=() => { save() } />
// Same properties and behavior, custom drawing.
<Button
label="Save"
on_click=() => { save() }
renderer=(ctx, button) => {
ctx.rounded_rect(
button.bounds,
radius=8,
fill=if button.pressed { pressed_color } else { idle_color }
)
ctx.text(button.label, within=button.bounds)
}
/>
Rich text editing belongs in the library
A notes app needs much more than a place to enter characters. It needs document structure, formatting, undo, paste handling, and often embedded content. Dia includes a modular editor framework inspired by ProseMirror.
Choose the extensions the document needs. A rich editor can add Markdown input, undo, and custom embeds:
let document = rich_document("# Notes")
<Editor
document=document
extensions=[
Markdown(shortcuts=true, input_rules=true, paste=true),
History(),
Embed(name="image", kind=.Block, view=Image),
Embed(name="mention", kind=.Inline, view=Mention),
]
/>
History should also work across the application. In a canvas editor, moving a shape and editing its label should participate in the same undo sequence.
let history = History()
<Editor document=note extensions=[History(shared=history)] />
<Button label="Undo" on_click=() => { history.undo() } />
Access to native libraries
Dia compiles to C and can import C headers. That gives applications access to existing libraries for storage, media, networking, and platform integration. The build still needs the library and its link configuration; the importer handles the declarations from the header.
extern "C" import "math.h" as math
let angle = 0.5
let horizontal = math.cos(angle)
Use the libraries you already know
For a game, I want getting started with an existing C library to be straightforward. Dia can read its header and expose its declarations directly.
extern "C" import "raylib.h" as rl
This lets you use a library such as raylib for a game while writing the application in Dia. Native compilation goes through C, and the compiler also has a WebAssembly target for browser builds. A particular library still needs to support the platform you’re targeting.
Automatic cleanup without a tracing collector
Rust is a major influence on Dia’s memory model. Dia uses ownership, value semantics, and deterministic cleanup, with reference counting for shared ownership. There is no tracing garbage collector, and ordinary owned values are cleaned up automatically.
struct Level {
name: String
}
fn rename(mut level: Level): void {
level.name = "Forest"
}
let original = Level{ name: "Untitled" }
var edited = copy original
rename(mut edited) // original is still "Untitled"
let saved = take edited // edited can no longer be used
// Each owned value is cleaned up when its scope ends.
Borrowing has restrictions that the compiler checks, but there are no lifetime parameters in the surface language. The aim is memory safety with code that remains practical to write for everyday application work.
For games, predictable cleanup and control over allocation matter. Reference counting and allocation still have costs; the design gives you ways to reason about them. Performance claims will need measurements on real programs.
Share suitable code between CPU and GPU
I also want to write shaders in Dia and reuse suitable functions on both the CPU and GPU. A calculation used by a simulation and its renderer should be expressible once when both targets support it.
fn brightness(rgb: Vec[3, f32]): f32 {
rgb.x * 0.2126 + rgb.y * 0.7152 + rgb.z * 0.0722
}
// CPU: use it in application code.
let preview = brightness(Vec[3, f32](0.8, 0.4, 0.2))
// GPU: call the same function from a shader.
@compute(workgroup: [8, 8, 1])
fn grayscale(src: Texture2D, dst: StorageImage2D, id: Vec[3, u32]): void {
let pixel = src.load(id.xy)
let gray = brightness(pixel.xyz)
dst.store(id.xy, Vec[4, f32](gray, gray, gray, pixel.w))
}
The GPU design restricts the types and operations available in shader code. Features such as recursion and reference-counted pointers are excluded, and the compiler checks functions called from a GPU entry point. This is part of the planned language, with implementation still in progress.
Build tools alongside the game
The reactive UI and text editing libraries are useful here too. Level editors, inspectors, dialogue tools, and asset browsers need application interfaces. I want those tools to share types and code with the game they support.
struct Light {
intensity: f32
}
let light = state(Light{ intensity: 1.0 })
<Row>
<GamePreview light=light />
<Slider @bind=light.intensity min=0.0 max=4.0 />
</Row>
What exists today
The compiler has native and WebAssembly code generation, C-header imports, ownership checking, structured concurrency, and a reactive runtime. The UI library, editor framework, and development tools are under active development.
Platform support and the standard libraries still need work. Dia is not ready for production use. The goal is a language I can use to build a complete application, with the common parts supplied and enough control to implement the specific parts myself.