DominScript is strongly typed, plugin-based, and built for deterministic performance work — graphics, media, networking, system tools. Three validation passes catch your bugs before the program even starts.
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; } /* 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 — distributed vote-counting systems, on-site collectors, 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.
DominScript is built on three commitments. They aren't slogans — every feature is checked against them.
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.
Three validation passes run before a single instruction executes. The parser checks syntax. The binder resolves types, variable scopes, definite assignment, and plugin ABI compatibility. The runtime handles dynamic range and type checks at execution. Bugs caught before they run.
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.
Each capability is an independent shared library — load what you need, ignore the rest. The runtime stays small while the surface area grows.
* In development — planned for upcoming releases. Further plugins under consideration: 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.A snapshot of what's working, what's in flight, and what's planned. The project is in active development; the test suite stands at 377 passing unit tests plus 41 example smoke tests, with identical baseline results on Linux and Windows.
The language is at spec version 0.9, with the parser, binder, runtime, and plugin system stable. A public v1.0 release is in preparation. Sign up below — we'll let you know the moment it's ready.
It's closer to embeddable, performance-oriented languages — think Lua, Tcl, or the role C extensions play inside Python — than to general-purpose languages like Python or Node.
If you need byte-precise control over data, want to drive native libraries like SDL2 / OpenGL / FFmpeg directly from script, and care about deterministic behavior, 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 377+ test suite as Linux — identical pass/fail counts, byte-for-byte. macOS and ARM / Raspberry Pi support are planned but not yet started — there are no native macOS or ARM binaries at v1.0.
Yes — a Visual Studio Code / VSCodium extension provides syntax highlighting, snippets, and plugin-aware completion and hover for all 32 official plugins (648 functions), generated straight from the source so it stays in lockstep with the reference.
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.
Licensing for the public release hasn't been finalized yet. Once decisions are made — including any source availability — they'll be announced ahead of v1.0.
v1.0 is in preparation. Leave your email and we'll let you know when it's ready — one message, no spam.