Packed binary data. Native plugins. Static validation before execution. No garbage collector. Two-way C integration. A scripting language for C applications where native data can stay native — without turning every buffer into a runtime object.
First downloadable release expected late September 2026.
Paradise of old coders.
In most scripting languages the answer is a pipeline: parse → allocate → convert to runtime objects → script → marshal back → native. In DominScript the answer is: lay a typed view over the bytes you already have. blob → typed view → script.
/* 9 bytes on the wire, byte-precise: u8 u8 u16 i32 u8 */ struct SensorFrame packet { u8 version; u8 kind; u16 sensorId; i32 value; u8 flags; } /* The frame handler. $Request aliases the bytes the listener received. */ callback i32 OnFrame(ref blob $Request) { $Request assign SensorFrame; /* typed view over the bytes that arrived */ if ($Request.version != 1) { return 0; } /* validate */ printf("sensor=%d value=%d\n", $Request.sensorId, $Request.value); $Request.value = $Request.value * 2; /* modify in place */ $Request.flags = 0x80; /* ack bit */ $Request = $Request; /* reply = these bytes */ return 1; }
The callback's ref blob is the listener's own receive buffer, not a copy of it. The TCP listener reads straight into that buffer.
assign builds nothing. It gives the bytes a typed shape: $Request.value is a 4-byte read at a fixed offset.
Field access on a view that is too short is a runtime error with file and line, not a silent read past the end.
The whole script — handler and a client in one file — is Examples/packet_view_demo.dom; it is also a regression test and an example smoke recipe. The numbers above describe the shipped code path. The reply reassignment is the one copy of the round trip: it is the response contract.
None of these is unique on its own. Together they make one coherent model for scripting next to native code: the script and the native program do not have to live in two separate worlds.
blob is raw bytes. A packet struct is a byte-precise layout you lay over them with assign. Data that is already in the right shape is not parsed, not boxed, not re-encoded. The language makes zero-copy the natural path; a plugin that copies does so by its own choice.
No GC pauses, no hidden object conversion, no allocation you did not write. Sized buffers, explicit casts, definite assignment. Data movement stays explicit — in a systems program you can tell what happens to the bytes.
Scripts call native plugins; native plugins call scripts back from their own threads, handing over a buffer by reference. The plugin API is a small, versioned C ABI. Your existing C code becomes a plugin in an afternoon.
Preprocessor, lexer, parser and binder run before execution: types, scopes, definite assignment, plugin signatures. The runtime keeps only the guards that cannot be static. Callbacks execute one at a time on a dispatcher thread, so script state needs no locks.
Without moving the data handling into a dynamic object model. That is the use case; these are its shapes.
opengl_ffmpeg_script_driven_demo.dom — the script that produces the demo above — opens an OpenGL window, decodes a video stream with FFmpeg into a spinning textured cube, handles mouse input, and runs at 60 FPS. Every drawing decision lives in the source; there is no rendering engine hidden behind a high-level façade.
The excerpt below shows the spine: plugin declarations, the packed render state struct, the two callbacks the host dispatches into, and the main lifecycle. Geometry helpers, draw functions, and the per-frame renderer are omitted — see the full source for those (about 600 lines total).
/* The runtime knows almost nothing on its own. Each `plugin` line loads a shared library that contributes a namespace (gl, ffmpeg, ...). */ plugin "builtin:os"; plugin "builtin:struct"; plugin "../plugins/blob/BlobPlugin"; plugin "../plugins/script_state/ScriptStatePlugin"; plugin "../plugins/opengl/GlPlugin"; plugin "../plugins/ffmpeg/FfmpegPlugin"; plugin "../plugins/image/ImagePlugin"; #define WINDOW_WIDTH 1280 #define WINDOW_HEIGHT 720 #define VIDEO_WIDTH 640 #define VIDEO_HEIGHT 360 #define AUTO_SPEED_X 0.23 #define AUTO_SPEED_Y 0.37 #define AUTO_SPEED_Z 0.11 /* Packed struct: byte-precise layout, no padding surprises. The runtime can overlay this onto any blob with `$X assign RenderState;` and read typed fields without copying. */ struct RenderState packet { i32 WindowWidth; i32 WindowHeight; i32 VideoSlot; i32 LogoSlot; double CameraDistance; double ViewScale; double AngleX; double AngleY; double AngleZ; double SpeedX; double SpeedY; double SpeedZ; /* ... edge color, cube gap, line width ... */ } /* =========== Callbacks the host dispatches into =================== */ /* Called by the ffmpeg plugin for each decoded video frame. The frame bytes arrive in $FramePacket as a borrowed, mutable blob -- no allocation, no memcpy. We hand them straight to the GL texture slot and return; the plugin reclaims ownership the moment we exit. */ callback i32 OnVideoFrame(ref blob $FramePacket) { ref blob &$S = blob.RefFromPointer(script_state.GetPointer(), script_state.GetSize()); $S assign RenderState; gl.UpdateTexture($S.VideoSlot, $FramePacket); return 1; } /* The event packet the gl plugin hands to the callback: a packed struct over the same bytes, so `$EventPacket assign GlEvent;` gives typed fields with no copy. */ struct GlEvent packet { u8 magic0; u8 magic1; u8 magic2; u8 magic3; u8 version; u8 type; u8 button; u8 pad0; i16 x; i16 y; i16 dx; i16 dy; i16 wheel; u16 width; u16 height; } /* Called by the gl plugin for each pumped input event. We pull the typed event view over the same packet bytes and update shared state. */ callback i32 OnOpenGlEvent(ref blob $EventPacket) { ref blob &$S = blob.RefFromPointer(script_state.GetPointer(), script_state.GetSize()); $S assign RenderState; $EventPacket assign GlEvent; if ($EventPacket.type == GL_EVENT_MOUSE_WHEEL) { $S.CameraDistance = $S.CameraDistance - (0.25 * (double)$EventPacket.wheel); $S.CameraDistance = ClampCameraDistance($S.CameraDistance); } /* ... mouse down / up / move handlers for left-drag rotation ... */ return 1; } /* =========== Entry point ========================================== */ i32 main() { i32 $Video = 0; blob $LogoPacket[0]; /* Open the GL window. gl.Open returns 0 if no display is available -- on a headless box the demo just exits cleanly. */ if (gl.Open(WINDOW_WIDTH, WINDOW_HEIGHT, "Domin OpenGL FFmpeg demo") == 0) { return 0; } /* Allocate the shared render state and initialize. Every callback and the main loop will bind a ref-blob view over these bytes -- no globals, no duplication; every caller sees the same data. */ script_state.Allocate(sizeof(RenderState)); { ref blob &$S = blob.RefFromPointer(script_state.GetPointer(), script_state.GetSize()); $S assign RenderState; $S.WindowWidth = WINDOW_WIDTH; $S.WindowHeight = WINDOW_HEIGHT; $S.CameraDistance = 4.2; $S.SpeedX = AUTO_SPEED_X; $S.SpeedY = AUTO_SPEED_Y; $S.SpeedZ = AUTO_SPEED_Z; $S.VideoSlot = gl.CreateTextureSlot(); $S.LogoSlot = gl.CreateTextureSlot(); /* ... starting angles, edge colors, cube gap ... */ } /* Load the logo and bind it to its texture slot. */ $LogoPacket = image.Load("Assets/Images/domin_logo.png"); /* ... bind $S, opengl.UpdateTexture($S.LogoSlot, $LogoPacket) ... */ /* Wire FFmpeg to deliver frames into OnVideoFrame at real-time pace. */ ffmpeg.SetOutputSize(VIDEO_WIDTH, VIDEO_HEIGHT); ffmpeg.SetRealtime(1); $Video = ffmpeg.Open("Assets/Videos/demo.mp4"); ffmpeg.StartAudio($Video); /* Main loop: pump input -> decode frame -> draw. */ while (gl.IsOpen() != 0) { gl.PumpEvents("OnOpenGlEvent"); if (gl.IsOpen() == 0) { break; } if (ffmpeg.DecodeNext($Video, "OnVideoFrame") == 0) { ffmpeg.Rewind($Video); } RenderScriptFrame(); /* draws the cube -- see full source */ } ffmpeg.Close($Video); gl.Close(); script_state.Free(); return 0; }
DominScript is not an academic language experiment. It's the deliberate distillation of decades of professional work — on-site data collectors, real-time statistics under broadcast pressure, performance-critical clients — into a reusable tool.
Every design choice traces back to a real problem: byte-precise schemas, packed structs, ref-style callbacks, zero-copy boundaries, plugin architecture. Comfort in the language means the engineer thinks about the task, not the tool.
In most languages, parsing is something you do because the data isn't yet in the right shape — bytes come in, objects come out, and a tax is paid in between. In DominScript, packed structs and zero-copy blobs mean the data is often already in the right shape; the parse step disappears. (The fastest parse is the parse you don't run.)
And when you genuinely need runtime code generation — REPLs, formula engines, config DSLs — validate-then-eval lets you do it without giving up the static guarantees the rest of the language gives you.
That is the entire goal: the script and the native program do not have to live in two separate worlds.
That is the design principle everything else follows from, and it explains why plugins carry so much weight here. DominScript does not try to build every library and every special case into itself. The language provides the safe, convenient scripting layer; the performance-critical and platform-specific parts are native plugins — and a hot script function is deliberately easy to move across that line. Three rules keep it honest.
The core knows almost nothing on its own. No built-in network, file, graphics, or OS calls. Every capability arrives through a plugin — a platform-specific shared library (.so on Linux, .dll on Windows). Scripts get exactly the powers their loaded plugins grant. Nothing more.
Static validation before execution; runtime guards where dynamic checks are unavoidable. The pipeline is preprocessor → lexer → parser → binder → runtime guards: the first four run before a single instruction executes (types, scopes, definite assignment, plugin signatures), the last keeps only what cannot be static — index and range checks. Every diagnostic carries at least file:line.
A hot script function should be mechanically and conceptually easy to move into a C plugin. The #define directive, brace blocks, typed signatures, and explicit casts all serve this goal. When performance demands it, the path from script to native is short and predictable.
The interpreter knows almost nothing on its own. Each capability is an independent shared library — load what you need, ignore the rest. 36 official plugins, 753 functions, every one documented and count-checked against the source.
→ See all 36 plugins and 753 functions · further plugins under consideration: log analysis · compression (gzip/zstd) · email (SMTP).
Need a capability that isn't shipping? Write your own plugin. DominScript ships with two complete skeletons you can copy as a starting point — one for event-driven plugins that call back into the script, and one for retain-style plugins that accept script-supplied data and keep their own copy. Fill in the parts specific to your task; the host integration, ABI, and lifecycle are already wired up.
A plugin is just a shared library (.so on Linux, .dll on Windows) exporting a fixed set of C entry points. Once compiled, a script picks it up with a single plugin "..." line at the top of the file.
No FFI bindings, no JNI-style glue, no marshaling layer between your code and the runtime. Your C function gets called directly when the script invokes it.
i8 i16 i32 i64, unsigned variants, f32 f64, bool, string, blob, plus user schemas. Explicit casts with exact-numeric runtime checks.
The binder traces every code path. Reading an uninitialized variable is a bind-time error, even across branchy while-if-break flow. The runtime never sees a half-set value.
A ref blob parameter aliases memory across the plugin / script line — in both directions. A plugin hands bytes to a callback without allocating; a script hands a blob to a plugin without the host copying first. No per-frame allocation, no host-object boxing — it's the same data on both sides. The plugin chooses when to retain.
Byte-precise struct layouts you can lay over a blob, like a C struct over a buffer. Predictable offsets, no padding surprises, native interop without bindings.
Parser, binder, and runtime check the script in order: syntax, types & scopes & plugin ABI compatibility, then dynamic range and type guards at execution. Most error classes are caught before the first instruction runs.
A single dispatcher thread runs every callback, one at a time — like a JavaScript event loop or Python's GIL. Two callbacks never execute concurrently, so script-level state shared between them is safe without you writing a single mutex. No hidden allocations, no surprise GC pauses, no fire-and-forget concurrency.
Build a script fragment at runtime, gate it with os.ValidateScript() to catch lexer / parser / binder errors before a single line runs, then os.Eval() it in the caller's own scope. REPLs, formula engines, config DSLs — without the "fingers crossed" pattern.
The VS Code / VSCodium extension runs the validator on every keystroke. The four static phases — preprocess, lexer, parser, binder — finish in milliseconds and surface each problem inline with exact line and column. Errors are red; non-fatal observations (an unused function, an orphan callback) come up in yellow. Both shapes carry the source label that tells you which phase produced them.
GetCubeFace_Geometry — defined but never called. A red error elsewhere in the file flags an unresolved call to the same name (visible in the Problems panel at line 334). Each entry is tagged with its source — dominscript (binder warning) vs. dominscript (binder error) — so you know which static phase noticed.The interpreter is a plain tree-walker. Its advantage appears when data already exists in native memory and the script needs to work with it without turning everything into scripting-language objects first — the whole data path gets simpler, not the inner loop faster. Two kinds of numbers, kept apart — and two platforms, measured on the same machine, shown as two separate tables.
| Same benchmarks, Linux | DominScript, ms | Python 3, ms | Elapsed time, shorter is better | DS / Py |
|---|---|---|---|---|
| Recursive Fibonacci(28)pure script · function calls | 80 | 24 | 3.33× | |
| Arithmetic loop, 10M iterationspure script · tight integer loop | 591 | 1033 | 0.57× | |
| Sieve of Eratosthenes, N=500kpure script · array indexing + loops | 84 | 91 | 0.92× | |
| Mandelbrot 200×200, 200 iterpure script · nested double arithmetic | 223 | 234 | 0.95× | |
| String find, 3 kB × 200kplugin-heavy · str plugin: memchr+memcmp | 27 | 54 | 0.50× | |
| JSON parse + read, 30kplugin-heavy · json plugin | 25 | 40 | 0.62× | |
| SHA-256, 1 kB × 20kplugin-heavy · crypto plugin (SHA-NI when the CPU has it, portable C otherwise) vs. Python's OpenSSL-backed hashlib | 11 | 12 | 0.92× | |
| Packet data path, 200k framesworkload · receive buffer → slice → typed view → fields → decision · Python: bytearray + struct.unpack_from | 60 | 66 | 0.91× | |
| Log lines, 100kworkload · build, find, slice, parse, format · Python: str methods | 63 | 62 | 1.02× |
| Same benchmarks, Windows | DominScript, ms | Python 3, ms | Elapsed time, shorter is better | DS / Py |
|---|---|---|---|---|
| Recursive Fibonacci(28)pure script · function calls | 113 | 35 | 3.23× | |
| Arithmetic loop, 10M iterationspure script · tight integer loop | 648 | 1278 | 0.51× | |
| Sieve of Eratosthenes, N=500kpure script · array indexing + loops | 100 | 128 | 0.78× | |
| Mandelbrot 200×200, 200 iterpure script · nested double arithmetic | 245 | 423 | 0.58× | |
| String find, 3 kB × 200kplugin-heavy · str plugin: memchr+memcmp | 29 | 66 | 0.44× | |
| JSON parse + read, 30kplugin-heavy · json plugin | 26 | 44 | 0.59× | |
| SHA-256, 1 kB × 20kplugin-heavy · crypto plugin (SHA-NI when the CPU has it, portable C otherwise) vs. Python's OpenSSL-backed hashlib | 13 | 17 | 0.76× | |
| Packet data path, 200k framesworkload · receive buffer → slice → typed view → fields → decision · Python: bytearray + struct.unpack_from | 71 | 80 | 0.89× | |
| Log lines, 100kworkload · build, find, slice, parse, format · Python: str methods | 83 | 70 | 1.19× |
The table above, read plainly: where the work is function calls (Fibonacci), the tree-walking interpreter pays about 3×. Where the loop body is arithmetic — integer or double — or indexes a typed array, DominScript sits below Python parity: the binder specializes provably-numeric statements into a small register machine, so those loops do not walk the tree at all. Where the work is done inside a plugin (string search, JSON, SHA-256), the plugin’s C code sets the pace. The two workload rows are program-shaped, not micro: the packet path — the thing this language is for — now runs ahead of Python’s struct; the log-lines row is level with Python now (1.0×) after a day spent on the string path, and it stays on the list until it is clearly ahead. The Windows table is the same tree built with mingw-w64 on the same machine: the interpreter rows land where they do on Linux (the mingw thread-local storage cost that used to make them 3–4× slower was removed from the hot path), and the string rows moved once the interpreter stopped copying string literals and plugin arguments it only needed to read (string find is now ahead of Python on both platforms); the log-lines row is the one still behind, and it stays on the list.
How many parses, copies, allocations and object conversions a socket → buffer → struct → decision → plugin path needs. On the shipped TCP listener path: 0 copies between the receive buffer and the script, 0 runtime object conversions, typed fields immediately (section 01). Per-call script→plugin and plugin→script overhead, blob hand-over at 1 kB / 64 kB, startup+validation time and steady event-loop latency are the next numbers to publish, measured the same way.
No cherry-picking: every benchmark script for both languages is in the repository, the two workload rows included, and the runner writes results.json. The numbers above will move; the method won't.
A snapshot of what's working, what's in flight, and what's planned. The project is in active development, currently at version 0.0.2.
The test suite covers the language, the runtime and all 36 native plugins, with 700+ functional tests and roughly 19,000 injected-failure cases per pass. The test system itself is written in DominScript. It reaches from language semantics to networking and native plugin interactions: the functional tests and 50+ example smoke runs are driven by one .dom runner that is make test on Linux and on Windows alike — plus the harnesses around it: TLS round-trips against a live OpenSSL server, mutual-TLS listeners, TCP/UDP/HTTP callback servers, database smoke tests, the debugger protocol, out-of-memory injection sweeps (~20 000 injected runs per pass), and AddressSanitizer runs of the whole suite. A language that can drive processes, sockets, files and its own interpreter well enough to be its own test framework has been exercised the way users will exercise it.
The interpreter is on the 0.0.x development line — currently 0.0.2. The first public release is milestone 0.1.0, gated on a release-readiness audit; the number is deliberately conservative, because under semantic versioning 0.x means the surface may still change. A 1.0 would be a promise of stability, and that promise is made once, when it can be kept. The parser, binder, runtime, and plugin system are in daily use and covered by a regression suite of several hundred tests; the first downloadable release will be announced on this page.
It sits next to embeddable scripting runtimes — Lua/LuaJIT, AngelScript, Wren, QuickJS, or Python used as an extension host. None of them is "worse"; the trade-off is different: DominScript keeps the data native and typed instead of converting it into a runtime object model, and lets the native side call the script back. If your problem is glue over a dynamic object graph, one of those is a better fit. If your problem is a binary buffer that has to stay a binary buffer, this is the one built for it.
If you need byte-precise control over data, want to drive native libraries like SDL2 / OpenGL / FFmpeg directly from script, and want explicit, predictable data handling (no GC, no hidden object conversion), DominScript fits. If you want a one-liner to rename some files, use whatever shell you already have.
Linux and Windows today. The native Windows build runs the same regression suite as Linux, through the same DominScript runner; the only differences in the verdicts are platform-specific skips (a POSIX-only shell recipe here, a missing runtime library there), never a failure on one side. macOS and ARM / Raspberry Pi support are planned but not yet started — there will be no native macOS or ARM binaries in the first release.
By itself. Since version 0.0.2 the entire test suite is written in DominScript: the manifest runner that drives the 700+ regression tests, and every harness around it — TLS round-trips with a live OpenSSL server, mutual-TLS listeners, out-of-memory injection sweeps, the debugger protocol, database smoke tests, the benchmark self-test. The same .dom runner is make test on Linux and on Windows; the retired shell and PowerShell twins are gone.
That is not a stunt. A language that can drive processes, sockets, files, and its own interpreter well enough to be its own test framework has been exercised in exactly the ways users will exercise it — and every bug the migration surfaced (there were several, on both platforms) was found by DominScript code reading DominScript output.
Yes — a Visual Studio Code / VSCodium extension provides syntax highlighting, plugin-aware completion and hover for all 36 official plugins (753 functions) — generated straight from the source so it stays in lockstep with the reference — plus a snippet pack of 131 templates: generic language scaffolds and plugin-specific patterns (2–5 per plugin, each naming the exact plugin load path it needs). A step debugger ships with it: breakpoints (including conditional ones), stepping, a Watch panel, and hover / Debug Console evaluation in the selected frame's real scope.
It also gives live diagnostics: the three static phases — lexer, parser, binder — run on save (or as you type) and surface errors inline with exact line and column, without executing your script. The extension ships alongside the preview; diagnostics use the DominScript binary, so they light up once you have a build.
A plugin is a shared library (.so on Linux, .dll on Windows) that exports a fixed C ABI. Scripts declare what they need with a plugin "..." directive at the top of the file. The runtime loads each plugin once at startup, validates the ABI, and unloads everything cleanly at exit.
Built-in plugins (blob, string, struct, os) are linked into the interpreter directly. Everything else lives as an external library that any DominScript build can pick up.
Functions marked with the callback keyword are reserved entry points the runtime can dispatch into — they aren't directly callable from script. Plugins (TCP listener, UDP server, HTTP listener, worker, timer) register callbacks and the host calls them when their event fires.
Borrowed buffers come in as ref blob parameters: zero-copy, mutable, and freed by the plugin once the callback returns. The host provides timeout, error, and shutdown policies plus introspection helpers like Phase(), StatusText(), and JobInfo().
Crucially, a single dispatcher thread runs every callback, one event at a time — the same model as a JavaScript event loop or Python's GIL. Plugin threads enqueue an event and wait; they never run script code themselves. The practical consequence: two callbacks never execute concurrently, so script-level state shared between them (a ref blob, a counter, a buffer) is race-free by construction. You don't write a mutex to protect ordinary script variables.
That question is genuinely still open — it is under consideration, and no decision has been made yet. What is decided: the first release will be a free download, together with the plugins completed by then, and free to use for individuals and companies alike — commercial use included. The exact license terms will be published soon. Free, in this case, does not automatically mean open source; whatever is decided on source availability will be announced here once the decision is made.
The first release (milestone 0.1.0) is in preparation, and it will be free to download and use: individuals and companies alike, commercial use included. The exact license terms will be published ahead of the release — check back here.