The 38 official plugins (str, map, json, …) are loaded from .so / .dll files in plugins/. The built-in namespaces — blob, string, os, struct, plugin, num and, since rev31zzti, print — live inside the runtime itself. They expose language-level facilities (capacity probes, formatting, CLI access, struct schema mutation, and plugin introspection) that wouldn't make sense as external libraries.
Note the namespace collision: the blob built-in is not the same as the blob plugin, and the string built-in is not the same as the str plugin. The built-ins add a handful of language-level accessors on top of the language's own blob and string types.
builtin:blob
One language-level accessor on the language's built-in blob type.
blob.Overflow
blob.Overflow($B) -> i32
Returns 1 if the last capacity-bounded write (:=) to $B truncated; 0 otherwise.
| Parameter | Type | Description |
|---|---|---|
| $B | blob | The blob to inspect. |
plugin "builtin:blob"; plugin "builtin:print"; void main() { blob $Cut[3]; blob $Src[4]; $Src[0] = 1; $Src[1] = 2; $Src[2] = 3; $Src[3] = 4; $Cut := $Src; // fits 3 of 4 bytes → truncates printf("overflow=%d\n", blob.Overflow($Cut)); // Output: overflow=1 }
$B.Fit()
$B.Fit(); // language method on any blob variable
Trims the blob’s physical capacity down to its logical length ($B.Length); the type itself is introduced in Language basics → Blobs. Not a plugin verb but a method of the language’s built-in blob type — it needs no plugin directive. Use it after shrinking .Length, or after a wide declaration you no longer need, to give the surplus bytes back; a later = assignment can grow the blob again, := stays bounded by the new capacity.
blob $B[16]; blob $Src[4]; $Src[0] = 10; $Src[1] = 20; $Src[2] = 30; $Src[3] = 40; $B = $Src; $B.Length = 2; // logical length down to 2 $B.Fit(); // capacity now 2 as well printf("len=%d byte1=%d\n", $B.Length, $B[1]); // Output: len=2 byte1=20
builtin:string
Capacity probes and formatted construction for the language's built-in string type.
string.Overflow
string.Overflow($S) -> i32
Returns 1 if the last := assignment to $S truncated at the declared capacity; 0 otherwise.
string $T[4]; $T := "abcdefgh"; // truncates to "abcd" printf("%d\n", string.Overflow($T)); // Output: 1
string.Capacity
string.Capacity($S) -> i32
Returns the current allocated capacity of $S in bytes (which may be larger than its length if = grew it, or equal to the declared bound if only := was used).
string $S[16] = "Hello"; printf("len=%d cap=%d\n", $S.Length, string.Capacity($S)); // Output: len=5 cap=16
$S.Fit()
$S.Fit(); // language method on any string variable
Trims the string’s allocated capacity down to its current length. A method of the language’s built-in string type (no plugin directive needed) — the counterpart of string.Capacity, which only reads the number. Typical use: a string declared with a generous bound (string $S[1024]) that has settled on a short value. After Fit() a plain = assignment of a longer text grows the capacity again; := is bounded by the trimmed size and reports through string.Overflow.
string $S[64] = "hello"; printf("len=%d cap=%d\n", $S.Length, string.Capacity($S)); $S.Fit(); printf("len=%d cap=%d\n", $S.Length, string.Capacity($S)); $S = "hello world, again"; // = grows it back as needed printf("len=%d cap=%d\n", $S.Length, string.Capacity($S)); // Output: // len=5 cap=64 // len=5 cap=5 // len=18 cap=18
string.Format
string.Format($Fmt, ...) -> string
Builds a new string using printf-style formatting. Returns the formatted result as a fresh string.
| Parameter | Type | Description |
|---|---|---|
| $Fmt | string | The format string (%d, %s, %f, …). |
| … | variadic | The values to format, matching the conversions in $Fmt. |
string $F[64] = string.Format("x=%d pi=%.2f s=%s", 42, 3.14, "ok"); print.Line($"$F"); // Output: x=42 pi=3.14 s=ok
builtin:os
Command-line arguments, environment variables, well-known paths, and (in the eval family) runtime script execution.
os.ArgCount
os.ArgCount() -> i32
Number of script-level arguments (positional argv-style; the interpreter and script name are not counted). The CLI contract is one rule: domin [host options...] <file.dom> [script arguments...] — everything before the script path belongs to the host (--validate, --warn-unused, --release, ...), everything after it is the script's, verbatim, even if it looks like a host flag; an unknown --option before the path is a usage error. os.ScriptPath() / os.ScriptDir() always refer to the .dom file regardless of how many host options precede it.
os.Arg
os.Arg($Index) -> string
Returns the script argument at $Index (0-based). An index outside 0 .. os.ArgCount()-1 is a runtime error (os.Arg index 7 is out of range (argc=2)), not an empty string — guard with os.ArgCount() or use os.FindArg / os.GetOption for name-based access.
plugin "builtin:os"; plugin "builtin:print"; void main() { i32 $I; printf("argc=%d\n", os.ArgCount()); for ($I = 0; $I < os.ArgCount(); $I += 1) { string $A[128] = os.Arg($I); print.Line($"[$I] $A"); } }
domin args.dom alpha --betaargc=2 [0] alpha [1] --beta
os.ScriptPath
os.ScriptPath() -> string
Absolute path of the running .dom script file.
os.ScriptDir
os.ScriptDir() -> string
Directory containing the running script. Useful for resolving paths relative to the script itself rather than to the working directory.
os.ExePath
os.ExePath() -> string
Absolute path of the DominScript interpreter binary.
os.Cwd
os.Cwd() -> string
The process's current working directory, absolute, /-separated on every platform. This is where the user launched the script from — relative paths the user typed resolve against it — whereas os.ScriptDir() is the script's own folder, which is what asset and include paths should be based on. Read-only by design: the language has no chdir, because the working directory is process-global and shared with the callback threads.
os.GetEnv
os.GetEnv($Name) -> string
Value of the host environment variable $Name, or an empty string when the variable is not set. The name must be non-empty — os.GetEnv("") is a runtime error. An empty result cannot tell "unset" from "set to empty" — use os.HasEnv for that distinction.
plugin "builtin:os"; plugin "builtin:print"; void main() { string $Home[256] = os.GetEnv("HOME"); string $Missing[16] = os.GetEnv("DOMIN_NO_SUCH_VARIABLE"); print.Line($"HOME=$Home missing=[$Missing]"); }
HOME=/home/alice missing=[]
os.HasEnv
os.HasEnv($Name) -> i32
1 when the host environment variable $Name exists — even with an empty value — and 0 when it is unset. This is the distinction os.GetEnv cannot make (it returns an empty string for both), so a script can honour the shell convention "set but empty = skip": if (os.HasEnv("X") && os.GetEnv("X").Length == 0) { /* skip */ }. The name must be non-empty (runtime error otherwise).
os.HasFlag
os.HasFlag($Name) -> bool
Returns true if any script argument exactly matches $Name (typically a --flag style string).
if (os.HasFlag("--verbose") == 1) { print.Line($"verbose mode on"); }
os.GetOption
os.GetOption($Name) -> string
Returns the value following $Name in the script arguments, e.g. with --port 8080 on the command line, os.GetOption("--port") returns "8080". Returns an empty string if $Name is not present. If $Name is the last argument, so nothing follows it, that is a runtime error (os.GetOption option '--port' is missing its value) — the same rule applies to GetOptionOrDefault, whose default only covers an absent option.
os.GetOptionOrDefault
os.GetOptionOrDefault($Name, $Default) -> string
Like GetOption, but returns $Default when the option is missing.
string $Port[16] = os.GetOptionOrDefault("--port", "8080"); print.Line($"port=$Port"); // Output (no --port given): port=8080
os.FindArg
os.FindArg($Text) -> i32
Index of the first script argument whose full text exactly matches $Text, or -1 when no argument matches. This is the positional counterpart of HasFlag: use it when you need what comes after a marker rather than just whether the marker is present.
plugin "builtin:os"; plugin "builtin:print"; void main() { i32 $At = os.FindArg("--config"); if ($At >= 0) { string $Next[128] = os.Arg($At + 1); print.Line($"config file: $Next"); } else { print.Line($"no --config given"); } }
config file: app.ini
Eval family — runtime script execution
The os.Eval family lets you compile and run a string of DominScript code at runtime, in the caller's function scope. os.ValidateScript performs the same static checks without executing.
os.ValidateScript
os.ValidateScript($Code) -> i32
Lexes, parses, and binds $Code as if it were going to run in the current scope. Returns 1 on success, 0 on failure. Not a runtime-success guarantee — only a static check. On failure, the os.LastValidation* getters carry the details.
os.Eval
os.Eval($Code) -> i32
Runs $Code as a continuation of the caller's function scope. New locals declared in the fragment are added to the caller's call frame and are released with it. The fragment sees the caller's variables, struct bindings, and plugin imports.
Not allowed in an eval fragment: plugin "..." directives, top-level function / struct / enum declarations. These are rejected as DISALLOWED validation errors.
i32 $Counter = 10; string $Code[64] = "$Counter = $Counter + 5;"; if (os.ValidateScript($Code) == 1) { os.Eval($Code); } print.Line($"$Counter"); // Output: 15
os.LastValidationCategory / Line / Column / Offset / Error / Warnings
os.LastValidationCategory() -> i32 // 0..6 (see table) os.LastValidationLine() -> i32 // 1-based, 0 if no location os.LastValidationColumn() -> i32 // 1-based, 0 if no location os.LastValidationOffset() -> i32 // byte offset, -1 if none os.LastValidationError() -> string // human-readable message os.LastValidationWarnings() -> string // binder warnings (rare)
Thread-local getters that describe the most recent ValidateScript / Eval failure on this thread.
| Value | Meaning |
|---|---|
| 0 | NONE — everything is fine |
| 1 | LEXER — invalid character, unterminated string, bad escape |
| 2 | PARSER — syntax error (missing ;, wrong structure) |
| 3 | BINDER — semantic error (type, scope, unknown variable) |
| 4 | PARAM — wrong argument to a plugin/built-in call |
| 5 | DISALLOWED — construct not permitted in an eval fragment |
| 6 | INTERNAL — internal error (OOM, unexpected state) |
builtin:struct
The struct keyword for schema definition, the assign statement that binds a schema to a blob or string, and four helpers for mutating schemas at runtime — either globally or per instance. For the whole picture with worked examples see Language basics → Structs over blobs.
assign (statement)
$Blob assign SchemaName; $String assign SchemaName;
Binds a declared schema to an existing blob or string variable. From that point the buffer's bytes are also reachable as named, typed members — $Blob.field, or $Blob[i].field when the buffer holds several elements. Nothing is copied: the schema is a view over the bytes the variable already owns, and each variable carries its own binding.
=
Binding is a statement, not an expression. $P = assign Point; is rejected by the parser: "Schema binding is now a statement, not an expression. Use '$X assign Name;' (no '=') instead." Older material that shows the = form predates this.
After binding, three properties describe the view — .HasStruct (0/1), .StructSize (bytes per element) and .Count (elements that fit) — and sizeof(SchemaName) gives the same element size at declaration time, which is the idiomatic way to size the buffer:
struct Point { i32 x; i32 y; } blob $P[sizeof(Point)]; $P assign Point; $P.x = 100; $P.y = 200; printf("%d %d %d\n", $P.HasStruct, $P.StructSize, $P.Count); // 1 8 1
On strings the members must be u8, and u8 name[N] declares an N-byte fixed-width field: reads return the slice as a NUL-terminated string, writes pad with spaces to the field width.
struct (keyword)
struct Pont { i32 x; i32 y; } struct Fejlec packet { u8 jelzo; i32 hossz; }
Top-level schema declaration. With the packet qualifier the layout follows the wire-packed convention (no padding between members); without it the natural alignment is used. Each declaration registers a globally addressable schema name.
AddStructMember
AddStructMember($SchemaName, $MemberName, $TypeStr, $InsertIdx)
Adds a member to a global struct schema at the given insertion index (0 = front, MemberCount = end). All future bindings of that schema see the new member; existing blobs are unaffected unless re-bound.
| Parameter | Type | Description |
|---|---|---|
| $SchemaName | string | Name of the schema to mutate (e.g. "Pont"). |
| $MemberName | string | New member name. |
| $TypeStr | string | Type literal (e.g. "i32", "string[64]", "blob[8]"). |
| $InsertIdx | i32 | Position. 0 inserts at the front, MemberCount appends. |
struct Pont { i32 x; i32 y; } AddStructMember("Pont", "z", "i32", 2); // append as third member
DelStructMember
DelStructMember($SchemaName, $MemberName)
Removes a member from a global schema. Existing blobs are unaffected unless re-bound.
AddBlobMember
AddBlobMember(ref $Blob, $MemberName, $TypeStr, $InsertIdx, $Migrate)Adds a member to this blob's per-instance schema (does not touch the global schema). $Migrate selects whether the existing blob bytes are physically reorganised:
$Migrate = 1— the blob is rewritten so existing members keep their values at their new offsets.$Migrate = 0— the blob bytes stay in place; only the schema view changes. Faster, but reads of moved members will see whatever bytes happen to lie there.
The type may be a bit field, written "u8:4" (an integer type, a colon, the width). On a packet schema an insertion then shifts everything behind it by the inserted width in bits, with no re-alignment — a member that sat on a byte boundary may stop doing so and becomes a bit-positioned member of its own width, readable and writable as before. The migration is one bit copy of the remainder, done once per array element (a 32-byte blob over a 5-byte struct holds 6 elements: all six are re-laid, the 2 tail bytes follow). Byte members in a packet schema go the same way (the shift is then whole bytes). A string, blob, array, float or double member cannot land off a byte boundary: such an insertion is refused and the schema stays as it was. An aligned (non-packet) schema takes no bit fields and keeps the byte-wise, padded migration. Positioned signals (@) are the other model: there nothing moves (AddStructSignal).
With $Migrate = 1 the whole blob is rewritten — every array element is re-laid (bit by bit on a bit-field schema) and the bytes after the last element are shifted too, on every call. On a large blob (hundreds of KB, MB) that is a lot of time, repeated per Add/Del. If a big blob’s layout changes, build the schema before filling the blob, or use $Migrate = 0 and write the data in the new layout; and prefer the signal verbs (AddStructSignal / AddBlobSignal), which never move anything.
struct Pont { i32 x; i32 y; } blob $B[8]; $B assign Pont; AddBlobMember(ref $B, "z", "i32", 2, 1); // migrate bytes
DelBlobMember
DelBlobMember(ref $Blob, $MemberName)Removes a member from a blob's per-instance schema. The blob bytes are not reclaimed; the schema view just no longer covers that field. With a third argument $Migrate = 1 the remaining members are moved down so they keep their values (the default, no third argument, moves nothing).
A delete with $Migrate = 1 rewrites the whole blob just like the insert — on a large blob that is relatively slow, and it happens on every call. See AddBlobMember. On a signal schema nothing ever moves, so $Migrate is refused there.
AddStructSignal
AddStructSignal($SchemaName, $Name, $TypeStr, $StartBit, $Width)
AddStructSignal($SchemaName, $Name, $TypeStr, $StartBit, $Width, $Order) // "big" / "little" / ""Adds a positioned signal (type name @ start : width [big|little], the line of a signal description) to a packet schema at run time — for a layout that is only known when the script runs: a description read from a file, a signal that turns up mid-run, a frame that carries different signals in another variant. The arguments are the declaration’s: the start bit on the schema’s grid (msb0 schemas count the drawing’s way), the width in bits (1..the type’s bits), an integer type (i8..u64, the type gives the sign), and the order — "little"/"intel" (the start bit is the LSB), "big"/"motorola" (the start bit is the MSB), "" or omitted = the schema’s.
Nothing moves. A signal owns the bits its start/width/order map to; the other members stay where they are. So there is no insertion index and no migration — the data stays in place, which is what a frame needs. DelStructMember / DelBlobMember on a signal schema behave the same way: the name goes, the bits stay (and $Migrate is refused). A [N] frame keeps its size and the signal must fit; a schema without [N] grows to the highest signal and shrinks on delete. An empty frame may be declared for exactly this use: struct Frame packet little [8] { }.
A view bound before the call keeps its old layout (copy-on-write, like AddStructMember); bind again to see the new signal. If the new signal shares a physical bit with an existing member, the same schema-overlap notice is printed as for a declaration — unless the schema is declared with overlap, which is the language’s union (see the guide). A schema with sequential bit fields (type name : N;) stays fixed at the declaration: a run-time signal needs the positioned (@) kind.
plugin "builtin:struct"; plugin "builtin:print"; // the hand-written frame (CanEngine in the guide) ... struct Decl packet little [8] { u16 rpm @ 0 : 16; i8 temp @ 16 : 8; u8 status @ 39 : 3 big; u16 load @ 47 : 12 big; u8 alive @ 63 : 1; } // ... and an empty frame the script fills from a signal table read at run time struct Dyn packet little [8] { } i32 main() { // signal line: rpm 0|16@1+ (start|length@order sign: @1 = Intel, @0 = Motorola, + unsigned, - signed) AddStructSignal("Dyn", "rpm", "u16", 0, 16, "little"); AddStructSignal("Dyn", "temp", "i8", 16, 8); // order omitted = the schema's (little) AddStructSignal("Dyn", "status", "u8", 39, 3, "big"); // status 39|3@0+ AddStructSignal("Dyn", "load", "u16", 47, 12, "motorola"); // "motorola" = "big" AddStructSignal("Dyn", "alive", "u8", 63, 1); blob $A[8]; blob $B[8]; $A assign Decl; $B assign Dyn; $A.rpm = 3000; $A.temp = -5; $A.status = 5; $A.load = 1234; $A.alive = 1; $B.rpm = 3000; $B.temp = -5; $B.status = 5; $B.load = 1234; $B.alive = 1; // $A and $B are byte-identical: B8 0B FB 00 A0 4D 20 80 // mid-run: this variant also carries a fault bit at bit 24 AddStructSignal("Dyn", "fault", "u8", 24, 1); $B assign Dyn; // bind again: the new signal is visible, the bytes are untouched if ($B.fault != 0) { printf("fault!\n"); } // the signal's position reads back like a declared one i32 $I = SchemaMemberCount("Dyn") - 1; printf("%s lsb=%d msb=%d w=%d %s\n", SchemaMemberName("Dyn", $I), SchemaMemberBitLsb("Dyn", $I), SchemaMemberBitMsb("Dyn", $I), SchemaMemberBitWidth("Dyn", $I), SchemaMemberBitOrder("Dyn", $I)); // fault lsb=24 msb=24 w=1 little DelStructMember("Dyn", "fault"); // the name goes, nothing moves return 0; }
Refused (a run-time error at the call): an aligned (non-packet) schema; a schema with sequential bit fields; a name already in the schema; a non-integer type (float, u8[4], string); width 0 or wider than the type; a negative start bit; an unknown order word; a signal running past a [N] frame; an unknown schema. The whole start×width×order×sign space is swept against an independent bit model in Tests/test_struct_signal_sweep.dom (6144 signals), with the same expected bytes as the declared sweep.
AddBlobSignal
AddBlobSignal(ref $Blob, $Name, $TypeStr, $StartBit, $Width) AddBlobSignal(ref $Blob, $Name, $TypeStr, $StartBit, $Width, $Order)
The same as AddStructSignal, but on this blob’s per-instance schema: the global schema and every other view stay as they are. The blob must already be bound ($Blob assign Name). No $Migrate argument — a signal never moves data. On a schema without [N] the signal must fit the blob’s bytes (runs past the blob otherwise); on a [N] frame it must fit the frame.
blob $F[8]; blob $G[8]; $F assign Dyn; $G assign Dyn; AddBlobSignal(ref $F, "status_byte", "u8", 32, 8); // only $F has it printf("%d %d\n", SchemaBlobMemberCount($F), SchemaBlobMemberCount($G)); // 6 5
ReplaceStructMember
ReplaceStructMember($SchemaName, $Name, $NewType) ReplaceStructMember($SchemaName, $Name, $NewType, $NewName)
Changes one member’s descriptor and nothing else: the member keeps its first bit and gets another type ("i16", "u8:3" …) and, optionally, another name. The other members stay where they are, no data moves, there is no migration — which is why there is no $Migrate argument. With the same width (u16 → i16, u8:3 → i8:3, i32 → float) the member simply reads as the new type. A narrower type keeps the member’s first bits and leaves a gap behind it. A wider type may only reach into free bits: running into another member is an error (a notice on an overlap schema), running past a [N] frame is an error. A signal schema is refused here (use ReplaceStructSignal). Copy-on-write as with AddStructMember: a view bound before the call keeps its descriptor, bind again.
If a member’s width has to change with the members behind it moving along and keeping their values, that is not a replace: it is the bit-stream insert / delete with $Migrate = 1 (AddBlobMember). A replace is deliberately the descriptor only.
struct S packet { u8 a : 3; u8 b : 3; u16 w; u8 tail; } $B assign S; $B.b = 6; $B.w = 0xBEEF; ReplaceStructMember("S", "w", "i16", "word"); // same width: the same bits, signed: -16657 ReplaceStructMember("S", "b", "u8:2"); // narrower: b keeps its first 2 bits (6 -> 2), a 1-bit gap, w stays at bit 8 ReplaceStructMember("S", "b", "u8:3"); // wider into the free bit: 6 again
ReplaceBlobMember
ReplaceBlobMember(ref $Blob, $Name, $NewType) ReplaceBlobMember(ref $Blob, $Name, $NewType, $NewName)
The same on this blob’s per-instance schema; the global schema and the other views stay. A wider type that would reach past the blob’s end is refused — a replace never grows the blob.
ReplaceStructSignal
ReplaceStructSignal($SchemaName, $Name, $TypeStr, $StartBit, $Width, $Order) ReplaceStructSignal($SchemaName, $Name, $TypeStr, $StartBit, $Width, $Order, $NewName)
A positioned signal re-placed, re-sized, re-typed and/or renamed in one step — the signal counterpart of the replace: nothing moves, the old bits stay as they are, the signal now reads the bits at its new position. The arguments are AddStructSignal’s ($Order is "big", "little" or "" for the schema’s). A sharing bit gives the same schema-overlap notice unless the schema has overlap; a refused call leaves the schema untouched.
ReplaceBlobSignal
ReplaceBlobSignal(ref $Blob, $Name, $TypeStr, $StartBit, $Width, $Order [, $NewName])The same on this blob’s per-instance schema only.
SetStructLayout
SetStructLayout($SchemaName, $Order, $Packing)
Changes the byte order ("big", "little", "native"; "motorola" / "intel" are accepted too) and the packing ("packet" or "aligned") of the program’s schema; "" keeps that part. Views bound afterwards see the new layout, views bound before keep the old one (copy on write), and there is no data to move at this level. An aligned layout puts C-style padding between the members, so their offsets change; a schema with bit fields or signals is packet by definition and refuses "aligned". The schema’s order is what byte members follow and the default for signals added later — a signal’s own order is bound when it is placed.
SetStructLayout("Pair", "big", ""); // order only SetStructLayout("Pair", "", "aligned"); // packing only
SetBlobLayout
SetBlobLayout(ref $Blob, $Order, $Packing, $Migrate)The same on this blob’s per-instance schema, with data in it — so the fourth argument says what stays put. $Migrate = 0 keeps the bytes and changes the reading: a raw frame you received, looked at as big-endian. $Migrate = 1 keeps the values: the bytes of every byte member are rewritten to the new order, and on a packing change every member is moved to its new offset. The blob grows when the new layout needs more room and is never cut. Signals keep both their bytes and their values either way (their order is bound at placement).
blob $F[8]; $F assign Frame; $F.id = 0x1234; SetBlobLayout(ref $F, "big", "", 1); // $F.id is still 0x1234, the bytes are now 12 34 SetBlobLayout(ref $F, "little", "", 0); // bytes untouched: $F.id reads 0x3412
Interactive frame building: start from an empty [N] { } frame, add members as the user describes them, and let the order and the packing be decided — or changed — at any point. See the Getting started example.
SetStructFrame
SetStructFrame($SchemaName, $FrameSize)
Fixes the schema’s frame at $FrameSize bytes (the [N] of the declaration, set at run time); every member must fit, the rest is padding. 0 frees the frame: the size follows the members’ extent again.
SetBlobFrame
SetBlobFrame(ref $Blob, $FrameSize)The same on this blob’s per-instance schema. The elements of the view are re-strided to the new size — the element count is kept — and the blob grows when the new stride needs more room; a shorter frame keeps the blob’s length (nothing is ever cut here; only := cuts).
SchemaByteOrder
SchemaByteOrder($SchemaName) -> string
SchemaBlobByteOrder(ref $Blob) -> stringThe schema’s byte order, resolved: "big" or "little" (a native schema reports the machine’s).
SchemaIsPacked
SchemaIsPacked($SchemaName) -> int
SchemaBlobIsPacked(ref $Blob) -> int1 for a packet schema, 0 for an aligned one.
builtin:plugin
Introspection over the plugin registry: list loaded plugins, look them and their functions up by name or index, and read each function's declared signature (parameter names, passing modes, type categories, arity). Everything here is metadata — nothing is called. Indices are 0-based; a plugin, function or parameter index outside the registry is a runtime error, a name lookup that fails returns -1.
plugin.Count
plugin.Count() -> i32
Number of plugins currently loaded (built-ins included).
plugin.Path
plugin.Path($Index) -> string
Returns the load path of the plugin at $Index. For built-ins this is the "builtin:<name>" identifier; for external plugins it is the resolved absolute path of the loaded shared library (for example /opt/domin/plugins/str/StrPlugin.so), not the relative text you wrote in the plugin directive. This exact string is the key FindPath matches against.
plugin.IsBuiltin
plugin.IsBuiltin($Index) -> bool
true if the plugin at $Index is one of the runtime built-ins; false if it was loaded from a shared library.
plugin.FunctionCount
plugin.FunctionCount($Index) -> i32
Number of functions exported by the plugin at $Index.
plugin.FunctionFullName
plugin.FunctionFullName($PluginIdx, $FunctionIdx) -> string
The fully qualified name of one of a plugin's functions (e.g. "str.Find"). Useful for diagnostic listings and dispatch tables built at runtime.
plugin "builtin:plugin"; plugin "builtin:print"; plugin "../plugins/str/StrPlugin"; void main() { i32 $N = plugin.Count(); i32 $I; for ($I = 0; $I < $N; $I += 1) { string $P[256] = plugin.Path($I); printf("%d %s (%d fn)\n", $I, $P, plugin.FunctionCount($I)); } }
0 builtin:plugin (29 fn) 1 builtin:print (14 fn) 2 /home/alice/domin/plugins/str/StrPlugin.so (41 fn)
Name lookups
plugin.FindPath
plugin.FindPath($Path) -> i32
Index of the loaded plugin whose registry path is exactly $Path, or -1. Built-ins match their "builtin:<name>" identifier. External plugins are registered under their resolved absolute library path, so the relative text from the plugin directive does not match — read the key back with plugin.Path first if you need to find an external plugin by path.
plugin "builtin:plugin"; plugin "builtin:print"; void main() { printf("builtin: %d\n", plugin.FindPath("builtin:plugin")); printf("relative text: %d\n", plugin.FindPath("builtin:print")); string $Key[256] = plugin.Path(1); printf("resolved key: %d\n", plugin.FindPath($Key)); }
builtin: 0 relative text: -1 resolved key: 1
plugin.FindFunction
plugin.FindFunction($PluginIdx, $FullName) -> i32
Index of the function named $FullName (as namespace.function, e.g. "str.Find") inside the plugin at $PluginIdx, or -1 when that plugin exports no such name. The lookup is per plugin — to search all of them, loop over plugin.Count().
plugin.FunctionNamespace
plugin.FunctionNamespace($PluginIdx, $FunctionIdx) -> string
The namespace part of one exported function (e.g. "str"), or an empty string when the export has none.
plugin.FunctionName
plugin.FunctionName($PluginIdx, $FunctionIdx) -> string
The bare function name of one export (e.g. "Find"). FunctionFullName joins the two with a dot.
Signature and arity
plugin.Signature
plugin.Signature($PluginIdx, $FunctionIdx) -> string
One readable summary line built from the stored metadata: namespace.function(name: type, refName: ref type, ...: type). Fixed parameters are listed by name; a trailing ... shows the variadic tail with its type category. Intended for diagnostics and generated listings, not for parsing back.
plugin "builtin:plugin"; plugin "builtin:print"; void main() { i32 $P = plugin.FindPath("builtin:plugin"); i32 $F = plugin.FindFunction($P, "plugin.Signature"); string $S[256] = plugin.Signature($P, $F); print.Line($"$S"); printf("params=%d min=%d max=%d\n", plugin.ParamCount($P, $F), plugin.MinArgs($P, $F), plugin.MaxArgs($P, $F)); i32 $PR = plugin.FindPath(plugin.Path(1)); i32 $PF = plugin.FindFunction($PR, "print.printf"); printf("%s\n", plugin.Signature($PR, $PF)); printf("max=%d unlimited=%d\n", plugin.MaxArgs($PR, $PF), plugin.ArgCountUnlimited()); }
plugin.Signature(pluginIndex: int, functionIndex: int) params=2 min=2 max=2 print.printf(format: string, ...: any) max=-1 unlimited=-1
plugin.ParamCount
plugin.ParamCount($PluginIdx, $FunctionIdx) -> i32
Number of named fixed parameters the function declares. Variadic arguments are not counted — see MaxArgs for the accepted call arity.
plugin.MinArgs
plugin.MinArgs($PluginIdx, $FunctionIdx) -> i32
Smallest number of arguments a call may pass.
plugin.MaxArgs
plugin.MaxArgs($PluginIdx, $FunctionIdx) -> i32
Largest number of arguments a call may pass, or -1 (the ArgCountUnlimited sentinel) for a variadic function with no upper bound.
plugin.ArgCountUnlimited
plugin.ArgCountUnlimited() -> i32
The sentinel MaxArgs returns for "no upper bound": -1. Compare against this rather than the literal so the intent reads in the script.
Parameters — names, modes, types
plugin.ParamName
plugin.ParamName($PluginIdx, $FunctionIdx, $ParamIdx) -> string
Declared name of fixed parameter $ParamIdx (0-based, below ParamCount), or an empty string when the plugin left it unnamed.
plugin.ParamMode
plugin.ParamMode($PluginIdx, $FunctionIdx, $ParamIdx) -> i32
Passing-mode code of one fixed parameter: by value or by reference. Compare with ModeValue() / ModeRef(), or turn it into text with ModeText.
plugin "builtin:plugin"; plugin "builtin:print"; void main() { i32 $P = plugin.FindPath("builtin:plugin"); i32 $F = plugin.FindFunction($P, "plugin.ModeText"); i32 $K; for ($K = 0; $K < plugin.ParamCount($P, $F); $K += 1) { printf("param %d: %s mode=%s type=%s\n", $K, plugin.ParamName($P, $F, $K), plugin.ModeText(plugin.ParamMode($P, $F, $K)), plugin.TypeText(plugin.ParamType($P, $F, $K))); } if (plugin.ParamMode($P, $F, 0) == plugin.ModeValue()) { print.Line($"first parameter is passed by value"); } }
param 0: code mode=value type=int first parameter is passed by value
plugin.ParamType
plugin.ParamType($PluginIdx, $FunctionIdx, $ParamIdx) -> i32
Type-category code of one fixed parameter. Compare with the Type* constants, or turn it into text with TypeText. The categories are the plugin ABI's coarse classes (any / int / float / string / blob / number), not the script-level widths such as i16.
plugin.VariadicMode
plugin.VariadicMode($PluginIdx, $FunctionIdx) -> i32
Passing-mode code declared for the variadic tail (the ... arguments). Meaningful only when MaxArgs exceeds ParamCount.
plugin.VariadicType
plugin.VariadicType($PluginIdx, $FunctionIdx) -> i32
Type-category code declared for the variadic tail. For print.printf this is TypeAny().
Constants and text conversion
plugin.ModeValue / plugin.ModeRef
plugin.ModeValue() -> i32 plugin.ModeRef() -> i32
The two passing-mode codes as constants, for comparing against ParamMode / VariadicMode. Use these rather than literal numbers — the numeric values are an ABI detail.
plugin.TypeAny / plugin.TypeInt / plugin.TypeFloat / plugin.TypeString / plugin.TypeBlob / plugin.TypeNumber
plugin.TypeAny() -> i32 plugin.TypeInt() -> i32 plugin.TypeFloat() -> i32 plugin.TypeString() -> i32 plugin.TypeBlob() -> i32 plugin.TypeNumber() -> i32
The six type-category codes as constants, for comparing against ParamType / VariadicType. TypeNumber means "int or float accepted"; TypeAny means unconstrained.
plugin.ModeText
plugin.ModeText($Code) -> string
"value" or "ref" for a mode code. Any other code — including one computed at runtime — is a runtime error (plugin.ModeText code 99 is invalid).
plugin.TypeText
plugin.TypeText($Code) -> string
"any", "int", "float", "string", "blob" or "number" for a type-category code; any other code is a runtime error.
plugin "builtin:plugin"; plugin "builtin:print"; void main() { printf("%s %s %s %s %s %s | %s %s\n", plugin.TypeText(plugin.TypeAny()), plugin.TypeText(plugin.TypeInt()), plugin.TypeText(plugin.TypeFloat()), plugin.TypeText(plugin.TypeString()), plugin.TypeText(plugin.TypeBlob()), plugin.TypeText(plugin.TypeNumber()), plugin.ModeText(plugin.ModeValue()), plugin.ModeText(plugin.ModeRef())); }
any int float string blob number | value ref
builtin:print
Console output — printf / PrintF, FastPrintF / fastprintf, Line / line, Print / print, and the stderr family Error, ErrorF, FastErrorF (each in both cases) — is a built-in namespace since rev31zzti: plugin "builtin:print";, no plugin file. The formatter is a checked wrapper over the system printf, and a literal format is verified at compile time (argument count and types, rejected directives). The full reference, the directive grammar and the error texts are on the print page.