Under development — the help is being filled in plugin by plugin.
Documentation · Language Basics

The fundamentals,
before your first program.

DominScript is a strongly-typed, plugin-based scripting language. The interpreter knows almost nothing on its own — capabilities arrive through plugin directives — and three validation passes catch whole classes of bugs before a program ever runs. This page walks through the core language: structure, types, variables, operators, control flow, and functions.

01

Core concept

DominScript is built on three pillars. Understanding them up front explains nearly every design decision in the language.

A minimal interpreter

The interpreter deliberately knows very little on its own. There is no built-in networking, file, graphics, or operating-system call. All of that arrives from plugins — platform-specific shared libraries (.so on Linux, .dll on Windows). A script has exactly the capabilities the loaded plugins provide, and no more.

Early error catching

Code passes through three phases before it runs:

  • Parser — syntactic correctness.
  • Binder — types, variable scope, definite assignment, and plugin ABI agreement.
  • Runtime — dynamic range and type checks during execution.

This makes it impossible to, for example, read an uninitialized variable or pass a wrongly-typed argument to a plugin function — those are binder errors, raised before the program starts.

C-translatability as a design rule

DominScript is intentionally C-like. 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 that goal.

Positioning

This does not mean full C compatibility. DominScript deliberately sits between a flexible scripting language and C — it borrows C's shape and discipline where they pay off, but stays smaller and more script-like elsewhere. The #define directive, for instance, is not (yet) a complete macro language. Treat the C resemblance as a guiding principle, not a compatibility promise.

Your first program

first.domDominScript
plugin "../plugins/print/PrintPlugin";

void main()
{
    i32 $Counter = 0;

    while ($Counter < 5)
    {
        printf("Count: %d\n", $Counter);
        $Counter += 1;
    }

    return;
}
terminalshell
domin first.dom
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
02

File structure

DominScript source files use the .dom extension. Every statement ends with a semicolon (;).

The plugin directive

Plugin directives may appear only at the top of the file, before any other statement.

imports.domDominScript
plugin "builtin:blob";
plugin "builtin:string";
plugin "builtin:struct";
plugin "builtin:os";
plugin "../plugins/print/PrintPlugin";
plugin "../plugins/file_io/FileIoPlugin";
plugin "../plugins/path/PathPlugin";
  • The extension is optional — the runtime appends the platform-appropriate one (.so / .dll) automatically.
  • The builtin:xxx form loads internal plugins written in C and linked into the interpreter.
  • Two plugins may not export the same namespace.function pair — that is a binder error.
  • There is no in-script plugin unload; everything is released automatically when the program ends.

The include directive

main.domDominScript
include "utils/helpers.dom";
include "../common/config.dom";

Textual inclusion, like C's #include. It works recursively, with cycle detection. Note that include paths are resolved relative to the including file.

Comments

comments.domDominScript
// Single-line comment

/* Multi-line
   comment */
Note

The # character is not a comment prefix — it belongs to the preprocessor.

03

Preprocessor — #define

The #define directive performs simple single-token text substitution — to a literal value, a keyword, or a previously defined name.

defines.domDominScript
#define MAX_SIZE     256
#define WAIT_FOREVER  -1
#define PI_APPROX     3.14159
#define STATUS_OK     0

void main()
{
    blob $B[MAX_SIZE];
    i32  $Status = STATUS_OK;
    double $Pi = PI_APPROX;

    printf("Size: %d, Pi: %f\n", MAX_SIZE, $Pi);
    return;
}

It is especially handy for naming switch/case labels, keeping branch logic readable.

Scope

This is plain token substitution, not a full C preprocessor. There are no function-like macros, conditional compilation, or token-pasting tricks — #define maps a name to a value, and that is intentionally all it does for now.

04

Types

Scalar types

TypeDescriptionSizeRange
boolboolean value1 bytetrue / false
i8signed integer1 byte−128 .. 127
u8unsigned integer1 byte0 .. 255
i16signed integer2 bytes−32 768 .. 32 767
u16unsigned integer2 bytes0 .. 65 535
i32signed integer4 bytes−2.15×10⁹ .. 2.15×10⁹
u32unsigned integer4 bytes0 .. 4.29×10⁹
i64signed integer8 bytes−9.2×10¹⁸ .. 9.2×10¹⁸
u64unsigned integer8 bytes0 .. 1.8×10¹⁹
floatsingle-precision float4 bytes~±3.4×10³⁸, ~7 digits
doubledouble-precision float8 bytes~±1.8×10³⁰⁸, ~15 digits

Compound types

TypeDescription
stringtext with a declared capacity — string $S[N]
blobraw byte array — blob $B[N]
voidvalid only as a return type

Type rules that matter

  • A void variable cannot be declared.
  • string and blob do not participate in implicit numeric conversion.
  • bool is not compatible with integer types at the plugin ABI level.
  • Scalar assignment does not allow implicit narrowing such as float → i32 — an explicit cast is required.
05

Variables

Variable names are prefixed with $. The compiler distinguishes a declaration (which fixes the type) from later assignments.

Declaration and initialization

vars.domDominScript
// Scalar types
i32    $Number  = 42;
double $Pi      = 3.14159;
bool   $Done    = true;
u8     $Bitmask = 0xFF;

// Declaration without initialization
i64 $Result;

// String — capacity is mandatory
string $Name[32];
string $City[64] = "Budapest";

// Blob — size is mandatory
blob $Data[128];
blob $Empty[0];

Scope rules

scope.domDominScript
void main()
{
    i32 $X = 10;           // outer scope

    {
        i32 $Y = 20;       // inner scope
        printf("%d\n", $X + $Y);
    }
    // $Y is no longer reachable here

    // FORBIDDEN: redeclaring $X in the same scope  -> binder error
    // FORBIDDEN: shadowing an outer $X in an inner scope -> binder error

    return;
}

Definite-assignment check

The binder verifies that every variable is definitely assigned before it is read. If a value is set on only some branches, reading it afterward is a binder error.

definite.domDominScript
i32 $Result;

if (GetSomeValue() > 0)
{
    $Result = 10;
}
else
{
    $Result = 0;       // assigned on every path
}

printf("%d\n", $Result);   // OK
06

Literals

Integer literals

int_literals.domDominScript
// Decimal
i32 $A = 42;
i32 $B = -17;

// Hexadecimal (0x / 0X prefix)
u8  $H1 = 0xFF;        // 255
i32 $H2 = 0x1A2B;      // 6699

// Binary (0b / 0B prefix)
u8  $B1 = 0b10101010;  // 170
i32 $B3 = 0b11110000;  // 240

// Negative hex / binary (unary minus)
i32 $Neg = -0x10;      // -16

Floating-point, string, boolean

literals.domDominScript
float  $F = 3.14;
double $D = 3.14159265358979;

string $S[32] = "Hello, world!";
string $Lines[64] = "First line\nSecond line";
string $Quote[32] = "she said: \"hi\"";
string $Path[16]  = "C:\\user";

bool $Yes = true;
bool $No  = false;

Supported escape sequences in string literals include \n, \t, \", \\, and \0 (NUL terminator).

07

Operators and expressions

Arithmetic

arithmetic.domDominScript
i32 $A = 10;  i32 $B = 3;

printf("%d\n", $A + $B);   // 13
printf("%d\n", $A - $B);   // 7
printf("%d\n", $A * $B);   // 30
printf("%d\n", $A / $B);   // 3  (integer division)
printf("%d\n", $A % $B);   // 1  (remainder)

Bitwise (integer types only)

bitwise.domDominScript
i32 $A = 0b00001100;  // 12
i32 $B = 0b00001010;  // 10

printf("%d\n", $A & $B);   // 8   AND
printf("%d\n", $A | $B);   // 14  OR
printf("%d\n", $A ^ $B);   // 6   XOR
printf("%d\n", ~$A);       // -13 NOT (sign-extending)
printf("%d\n", 1 << 4);     // 16  left shift
printf("%d\n", -8 >> 1);    // -4  arithmetic right shift
Runtime check

Shift counts must fall between 0 and 63.

Comparison & logical

compare.domDominScript
bool $R1 = ($A == $B);   // equal
bool $R2 = ($A != $B);   // not equal
bool $R3 = ($A <= $B);   // less-or-equal

bool $L1 = ($X && $Y);   // logical AND
bool $L2 = ($X || $Y);   // logical OR
bool $L3 = !$X;          // logical NOT

String and blob concatenation (+)

concat.domDominScript
string $First[32] = "Hello";
string $Both[64];
$Both = $First + ", world!";   // Hello, world!

blob $A[3];  blob $B[3];  blob $C[0];
$C = $A + $B;                  // 6-byte blob: A then B

Operator precedence (high → low)

LevelOperators
1 (highest)!  ~  unary -
2*  /  %
3+  -
4<<  >>
5<  <=  >  >=
6==  !=
7 – 9&  then  ^  then  |
10&&
11 (lowest)||
08

Assignment & compound operators

Compound operators are desugared by the parser: $X op= Y becomes $X = $X op Y.

compound.domDominScript
i32 $X = 100;

$X += 5;     // 105
$X -= 10;    // 95
$X *= 2;     // 190
$X /= 4;     // 47
$X %= 10;    // 7

$X &= 0xFF;  $X |= 0x01;  $X ^= 0x0F;
$X <<= 2;   $X >>= 1;
Restriction

A compound operator may be applied only to a simple variable. Index, range, or member targets such as $Array[0] += 5 or $S.x += 1 are binder errors.

Capacity-bounded assignment: = vs :=

Strings and blobs have a second assignment operator, :=. The two differ in exactly one thing: := never grows the target's allocated capacity, whereas = grows it as needed.

= (plain):= (capacity-bounded)
Upper boundnone — capacity grows if neededthe target's declared capacity
Source too longcapacity grows to fittruncated at capacity; Overflow becomes 1
Shorter sourcethe remainder is dropped (new length = source length)the existing tail beyond the source is kept
Typical usegeneral assignmentfixed-size fields — e.g. a string member of a packed struct
string $S[4];
$S = "abcdefgh";        // '=' grows the capacity
// $S = "abcdefgh", Length 8, Capacity 8, Overflow 0

string $T[4];
$T := "abcdefgh";       // ':=' truncates at the capacity
// $T = "abcd", Length 4, Capacity 4, Overflow 1

string $A[16] = "Hello";
$A := "AB";             // shorter source: the existing tail is kept
// $A = "ABllo", Length 5, Overflow 0
Same rule for blobs

Blobs behave identically. Writing into a fixed-size blob with := — including at an offset, e.g. $Cut[1] := $Src[0..3] — will not grow it: it truncates at the capacity and flags Overflow, whereas = grows the blob to fit.

09

Explicit casts

An explicit cast is required between scalar types whenever the conversion is narrowing or non-trivial. Many casts are also validated at runtime — the value must actually fit.

casts.domDominScript
// float/double -> integer (only if the value is whole)
float $F = 3.0;
i32   $I = (i32)$F;     // OK: 3.0 is whole
i32   $J = (i32)3.7;    // RUNTIME ERROR: 3.7 not whole

// narrowing integer cast
i8    $Ok  = (i8)127;     // OK
i8    $Bad = (i8)300;     // RUNTIME ERROR: 300 does not fit i8

// unsigned / signed
u32  $U = (u32)-1;       // RUNTIME ERROR: -1 is negative

// chainable
double $Val = (double)(i16)7;

// casts to/from string, blob, or void are binder errors
10

Control flow

if / else if / else

branch.domDominScript
if ($Value > 100)
{
    printf("large\n");
}
else if ($Value > 0)
{
    printf("small\n");
}
else
{
    printf("zero or negative\n");
}

The elseif and else if spellings are equivalent.

while, for, do/while, do/until

loops.domDominScript
i32 $I = 0;
while ($I < 10) { $I += 1; }

for ($I = 0; $I < 5; $I += 1)
{
    printf("%d\n", $I);
}

// runs at least once, while condition is TRUE
do { $I += 1; } while ($I < 3);

// runs at least once, until condition is TRUE
do { $I -= 1; } until ($I <= 0);

switch / case

switch.domDominScript
switch ($Err)
{
    case ERR_NONE: printf("none\n"); break;
    case ERR_IO:   printf("i/o\n");  break;

    // fallthrough: a case without break falls into the next
    case 6:
    case 7:
        printf("weekend\n");
        break;

    default: printf("unknown\n"); break;
}

break, continue, goto, return

jumps.domDominScript
for ($I = 0; $I < 10; $I += 1)
{
    if ($I == 3) { continue; }   // skip 3
    if ($I == 7) { break; }      // stop at 7
    printf("%d\n", $I);
}
// prints: 0 1 2 4 5 6

start:
    if ($I >= 5) { goto done; }
    $I += 1;
    goto start;
done:
    return;
11

Functions

Basic syntax

functions.domDominScript
// return type + name + parameters
i32 Sum(i32 $A, i32 $B)
{
    return $A + $B;
}

void PrintNumber(i32 $N)
{
    printf("Number: %d\n", $N);
    return;
}

Value vs ref parameters

Value parameters receive a copy; ref parameters receive a reference, so changes are visible on the original. Type matching on ref parameters is strict — an i64 argument will not bind to a ref i32 parameter.

refs.domDominScript
void Increment(i32 $X)      { $X += 1; }       // copy
void IncrementRef(ref i32 $X) { $X += 1; }  // reference

void main()
{
    i32 $A = 10;
    Increment($A);      printf("%d\n", $A);   // 10
    IncrementRef($A);   printf("%d\n", $A);   // 11
    return;
}

Blob / string ref parameters & recursion

advanced.domDominScript
void FillBlob(ref blob $B, i32 $Value)
{
    i32 $I;
    for ($I = 0; $I < $B.Length; $I += 1)
    {
        $B[$I] = $Value;
    }
}

i32 Factorial(i32 $N)
{
    if ($N <= 1) { return 1; }
    return $N * Factorial($N - 1);   // 5! = 120
}

Calling plugin namespaces

Plugin functions are called in namespace.Function(...) form. (printf is an exception: the print plugin exports it directly, without a namespace.)

namespaces.domDominScript
plugin "../plugins/print/PrintPlugin";
plugin "../plugins/path/PathPlugin";
plugin "../plugins/file_io/FileIoPlugin";

void main()
{
    string $Norm[64] = path.Normalize("a/./b/../c");
    printf("%s\n", $Norm);                // a/c

    fileio.WriteFileText("out.txt", "content");
    bool $Exists = fileio.Exists("out.txt");
    printf("exists: %d\n", $Exists);
    return;
}
Next

That covers the core language. The section below walks through how plugins and callbacks actually move data through the runtime; the per-plugin reference lives one click away.

12

Plugins & callbacks: how data flows

DominScript plugins fall into four shapes. Adapter plugins are thin synchronous wrappers around an existing C library — every script call is one library call (e.g. json over cJSON, sqlite over libsqlite3, crypto over OpenSSL). Bridge plugins wrap a library too, but additionally mediate callbacks: sdl, gl, ffmpeg and image expose synchronous calls and deliver events to script callbacks through the host's callback queue. Callback-driven plugins like callbacktcp or callbackworker exist primarily to deliver events — their own OS threads enqueue work onto the same central queue. (Plus a fourth, plain group: native plugins such as str, map or csv — pure C implementations, wrapping nothing.) The two interesting data paths are below.

The callback model

Every callback in DominScript flows through the same machinery, regardless of which plugin produced the event (TCP/UDP listener, HTTP listener, worker pool, timer, callback listener). One single dispatcher thread drains a central FIFO queue and runs callbacks strictly one at a time. The plugin's own thread blocks until its callback finishes. This is a load-bearing invariant — two script-side callbacks never run simultaneously.

PLUGIN THREAD HOST RUNTIME SCRIPT SIDE TCP / UDP / timer / worker / … single dispatcher callback function Event arrives TCP packet, timer tick, worker result, … uint8_t *buf; size_t len; — bytes in plugin's own buffer host->InvokeBorrowedCallback( handle, buf, len, timeoutMs, Release, …) Wraps bytes in a DominBlob .Data = buf (no copy) .Size = len .Flags = READONLY|EXTERNAL .Release = plugin's release fn Enqueue on central queue FIFO, mutex-guarded. Every plugin's events merge here: TCP, UDP, timer, worker, listener — one queue. Plugin thread BLOCKS Sleeps on the event's CompletionCond until the dispatcher signals. ONE dispatcher dequeues Runs callbacks strictly one at a time. Two callbacks never overlap. borrowed view Script callback runs callback blob Handle( blob $Request) { // reads $Request (read-only) blob $resp[N] = …; return $resp; } $Request aliases the borrowed buffer — no copy. READONLY flag enforced. response blob OR i32 Wraps response, signals Stores ResponseBlob (refcount=1) + RefResult on the CallbackEvent, BroadcastCondVar(CompletionCond) wake Plugin wakes Receives ResponseBlob, sends bytes back to OS (TCP write, store, …), then host->ReleaseBlob(ResponseBlob).
  1. ① Event arrives. A plugin OS thread reads bytes from a socket, fires a timer, or finishes a worker job. The bytes sit in the plugin's own buffer.
  2. ② InvokeBorrowedCallback. The plugin calls the host's InvokeBorrowedCallback with the pointer, size, timeout, and an optional Release function pointer. No deep copy is made.
  3. ③ Wraps a borrowed blob. The host builds a lightweight DominBlob whose .Data aliases the plugin's bytes. The flags READONLY | EXTERNAL guard against script-side writes and tell the host that the bytes are owned externally.
  4. ④ Enqueue. The host queues a DominCallbackEvent on the central FIFO callback queue.
  5. ⏸ Plugin blocks. The plugin's own thread sleeps on the event's completion condition variable. It does not run the script callback itself.
  6. ⑤ One dispatcher. A single, long-lived dispatcher thread drains the queue. Two script callbacks never run at the same time, so shared script state seen by callbacks is not subject to a data race.
  7. ⑥–⑦ Script callback runs. The dispatcher invokes the registered script function with $Request as a read-only view aliasing the borrowed buffer.
  8. ⑧ Return. The script returns a response blob, or (for ref-style callbacks) an i32 plus a ref blob.
  9. ⑨ Signal completion. The host stores the response on the event and broadcasts the cond var.
  10. ⑩–⑪ Plugin resumes. The plugin wakes, takes the ResponseBlob, sends or stores the bytes, and releases it.
Where the blob is destroyed

Three lifetimes are at play:

  • The plugin's own buffer (the bytes buf). After the script callback returns, the host frees the borrowed wrapper, which fires the plugin-supplied Release function on buf. This is the moment the borrowed bytes are reclaimed. If the plugin passes NULL for Release (the skeleton's path), the synchronous call simply held the bytes alive for its duration and the plugin keeps ownership afterwards.
  • The borrowed-blob wrapper (the small DominBlob struct that the host built in step ③). Freed automatically when the script callback returns; the script never sees it again.
  • The response blob (built by the script in step ⑦). Owned by the host; refcounted. The plugin must call host->ReleaseBlob when done — that is when the response bytes are freed.

Adapter plugins — the contrasting model

Pure adapters (json, sqlite, crypto) have no threads, no queue, no callbacks. Every script-side call is a thin layer over exactly one call into an underlying C library (cJSON, libsqlite3, OpenSSL). They are stateless from the runtime's perspective; control flow is plain, synchronous, and lives on the same thread as the caller. Blobs do cross the boundary — crypto.Sha256($Data) takes a blob in and returns a blob out — but they don't travel the queue/dispatcher path: the plugin reads the script's bytes directly while the synchronous call holds them alive.

SCRIPT ADAPTER PLUGIN C LIBRARY caller thin wrapper cJSON · libsqlite3 · OpenSSL · … i64 $age = json.GetInt( $h, "user.age"); call Validate args, resolve $hcJSON *, walk dotted path. one C call cJSON_GetObjectItem(...) cJSON *node return Convert C value → Result->As.IntValue = … return Script resumes with $age = 42
The third shape: bridge plugins

sdl, gl, ffmpeg, image wrap an external library and deliver events via callback. sdl.SetEventCallback and gl.PumpEvents use the same RegisterCallback + InvokeBorrowedCallback path shown in the first diagram — events from the underlying library funnel through the central queue and the single dispatcher, exactly as a TCP packet does.

Two data paths, in one line

Adapter plugins are synchronous: the script call returns on the same thread, no event queue, no dispatcher involvement; blobs cross the boundary by direct pointer while the call holds. Callback-driven and bridge plugins are queue-merged: every event funnels through one dispatcher, and the payload crosses the boundary as a borrowed (or refcounted) DominBlob.

No sections match “”. Try a different term.