KonsolScript is modern lightweight embeddable scripting language optimized for tooling, games, automation, and beginner accessibility designed for someone who values clarity, portability, and extensibility.
Three primitive types, all stored in a Value variant:
| Type | Declaration keyword | Default |
|---|---|---|
| Number | Var:Number |
0 |
| String | Var:String |
"" |
| Boolean | Var:Boolean |
false |
Var:Number x;
Var:Number y = 42;
Var:String name = "hello";
Var:Boolean flag = true;
Var:Number a = 1, b = 2, c = 3;
Variables can be assigned without re-declaring:
x = x + 1;
name = "world";
42 // integer
3.14 // float
0xFF // hex (stored as Number)
"hello\n" // string — supports \n \t \" \\
"Hi ${name}!" // interpolated string — ${expr} is evaluated at runtime
true false // booleans
Embed any expression inside ${ } within a double-quoted string:
Var:String name = "world";
Konsol:Print("Hello ${name}!"); // Hello world!
Var:Number a = 3;
Var:Number b = 4;
Konsol:Print("${a} + ${b} = ${a + b}"); // 3 + 4 = 7
Var:String msg = "Result: ${a * b}";
Konsol:Print(msg); // Result: 12
$ not followed by { is treated as a literal character ("$5.00" stays $5.00).\n, \t, \", \\) work inside interpolated strings.${}${} accepts any expression the runtime can evaluate — not just variable names. Arithmetic, ternary, function calls, and even nested interpolation all work. This puts KonsolScript in the same class as **Kotlin**, **Dart**, **JavaScript** template literals, **Python** f-strings, **C#** interpolated strings, and **Ruby/Elixir** #{} — languages where ${} is a full expression slot, not merely a variable substitution.
Konsol:Print("area = ${width * height}");
Konsol:Print("midpoint = ${(a + b) / 2}");
Konsol:Print("sign = ${x > 0 ? "pos" : x < 0 ? "neg" : "zero"}");
Konsol:Print("doubled = ${timesTwo(n)}");
When the expression inside ${} gets complex, factor it into a variable first:
// discouraged — legal but hard to scan
Konsol:Print("${x > 0 ? "pos" : x < 0 ? "neg" : "zero"}");
// preferred
Var:String sign = x > 0 ? "pos" : x < 0 ? "neg" : "zero";
Konsol:Print("sign: ${sign}");
// single-line comment
/* multi-line
comment */
#definePlain word-boundary text substitution before tokenisation:
#define MAX 100
#define PI 3.14159
Var:Number limit = MAX;
#includeTwo forms:
| Syntax | Behaviour |
|---|---|
#include "name.ks" |
Always loads a KonsolScript file |
#include "name" |
Plugin-first: searches for name.dll / .so / .dylib, then libname.dll / .so / .dylib, falls back to name.ks |
KonsolScript file includes
#include "lib/utils.ks"
#include "helpers.ks"
#include other files.Plugin includes
#include "kse_sample" // loads kse_sample.dll / .so from plugin search paths
Plugin search order for #include "name":
/plugins/name.dll /plugins/name.dll engine.addPluginPath() or the --plugin-path CLI flagMINKS_PLUGIN_PATH env var (colon-separated on Linux/macOS; semicolon on Windows)~/.minks/plugins/ (Linux/macOS) — %APPDATA%\minks\plugins\ (Windows)./plugins/If the library is not found, minks falls back to searching for name.ks. If that also fails, a runtime error is raised. Each plugin name is include-guarded — including the same name twice is a no-op.
// lib/math_utils.ks
function clamp(val, lo, hi) {
if (val < lo) { return lo; }
if (val > hi) { return hi; }
return val;
}
// main.ks
#include "lib/math_utils.ks"
clamp(200, 0, 100);
Konsol:Print(_ret); // 100
Function parameters may be written as bare names (no type annotation) or with a type prefix — both forms work in included and non-included files:
function add(a, b) { return a + b; } // bare names
function add2(Number a, Number b) { return a + b; } // typed
If the script defines a main function, it is called automatically. Global variable declarations are still executed first to initialise globals.
Var:Number count = 0; // initialised before main() runs
function main() {
Konsol:Print(count);
}
If there is no main, all top-level statements execute in order.
| Op | Meaning |
|---|---|
+ |
Add / concat |
- |
Subtract |
* |
Multiply |
/ |
Divide |
% |
Modulo |
^ |
Power |
+ on a string and any value concatenates both as strings.
== != > >= < <=
&& || !
= += -= *= /= %= ++ --
Unary (-, !) → ^ → * / % → + - → comparison → && → ||
if (x > 10) {
Konsol:Print("big");
} else if (x > 5) {
Konsol:Print("medium");
} else {
Konsol:Print("small");
}
Var:Number i = 0;
while (i < 5) {
Konsol:Print(i);
i++;
}
for (Var:Number i = 0; i < 10; i++) {
Konsol:Print(i);
}
The init clause can use Var:Number declaration or a plain assignment. The increment supports ++, --, and = expr.
while (true) {
if (done == true) { break; }
}
Skips the rest of the current iteration and jumps to the next. In a for loop the increment still runs.
for (Var:Number i = 0; i < 10; i++) {
if (i % 2 == 0) { continue; }
Konsol:Print(i); // prints odd numbers only
}
Evaluates an expression and executes the matching case block. Works with numbers and strings. Supports fall-through (omit break) and a default branch.
switch (expr) {
case value1:
// statements
break;
case value2:
// statements
break;
default:
// statements
}
break exits the switch. continue inside a switch propagates to the enclosing loop. Nested switches are supported.
Var:Number x = 2;
switch (x) {
case 1:
Konsol:Print("one");
break;
case 2:
Konsol:Print("two");
break;
default:
Konsol:Print("other");
}
Var:String day = "Wed";
switch (day) {
case "Mon": Konsol:Print("Monday"); break;
case "Wed": Konsol:Print("Wednesday"); break;
default: Konsol:Print("another day");
}
Fall-through example (no break on case 1):
switch (n) {
case 1:
Konsol:Print("one or two");
case 2:
Konsol:Print("two");
break;
}
Iterate over a **List**, **Array**, **Map** (yields keys, sorted), or **String** (yields characters) without a manual index counter.
foreach (varName in collectionName) {
// body — varName holds the current element / key
}
break and continue work the same as in for / while.
List:String fruits;
List:Push(fruits, "apple");
List:Push(fruits, "banana");
List:Push(fruits, "cherry");
foreach (item in fruits) {
Konsol:Print(item); // apple banana cherry
}
// Map — keys yielded in sorted order
Map:New cfg;
Map:Set(cfg, "host", "localhost");
Map:Set(cfg, "port", "8080");
Var:String val;
foreach (key in cfg) {
Map:Get(cfg, key, val);
Konsol:Print(key + " = " + val);
}
// String — one character per iteration
Var:String word = "hi";
foreach (ch in word) {
Konsol:Print(ch); // h i
}
function greet(String name) {
Konsol:Print("Hello, " + name);
}
function add(Number a, Number b) : Number {
return a + b;
}
greet("world");
Var:Number result = add(3, 4);
Parameter types can be written as Number x or Var:Number x — both work.
Return type annotation (: Number, : String, : Boolean) is optional and currently informational.
Functions can be called in expressions:
Var:Number n = add(1, 2) * 10;
Recursion is supported. Each call gets its own local scope; locals are erased after the call.
Fixed-size typed arrays. Size is set at declaration and does not change. Use List for dynamic resizable collections.
Array:New name[size]:Type;
size can be a literal or a variable expression. Type is Number, String, or Boolean.
Array:New scores[5]:Number;
Array:New names[3]:String;
Array:New flags[4]:Boolean;
Var:Number n = 10;
Array:New buf[n]:Number; // size from variable
Elements are zero-initialised: 0, "", or false.
scores[0] = 95;
scores[1] = 87;
Konsol:Print(scores[0]); // 95
Index reads out of bounds return 0 / "" / false. Index writes out of bounds are silently ignored.
for (Var:Number i = 0; i < 5; i++) {
Konsol:Print(scores[i]);
}
foreach (s in scores) {
Konsol:Print(s);
}
Array and List currently share the same internal Value storage, so there is no runtime performance difference between them. Array is kept as a distinct type to leave room for a typed contiguous-storage optimisation in a future release.
Simple data-only record types. No methods.
Var:Create(Point, Number x, Number y);
Var:Point p;
p.x = 10;
p.y = 20;
Konsol:Print(p.x + p.y);
Var:Create is processed at symbol-build time. Multiple fields separated by commas.
Full OOP with fields and methods.
Class:Create(Animal) {
Var:String name;
Var:Number age;
function setName(String n) {
name = n;
}
function speak() {
Konsol:Print(name + " says hello");
}
function getAge() : Number {
return age;
}
}
Fields are declared with Var:Type fieldName; inside the body. Methods are declared with function. Inside a method, fields are accessible directly by name.
Class:Animal dog; // single instance via Class:TypeName
Var:Animal cat; // equivalent shorthand: Var:TypeName
dog.name = "Rex";
dog.age = 3;
dog.setName("Buddy");
dog.speak();
Var:Number a = dog.age;
Field access for reading uses instance.field. For writing, assign directly: instance.field = value.
Method calls use instance.method(args). Modified fields are written back to the instance after each method call.
List:Animal pack; // list of Animal instances
Array:New herd[10]:Animal; // array of Animal instances
Each Class:TypeName varName (or Var:TypeName varName) creates an independent instance with its own copy of the fields.
try {
throw "something went wrong";
} catch (String e) {
Konsol:Print("Caught: " + e);
} finally {
Konsol:Print("always runs");
}
throw accepts any expression; the string value becomes the exception message.(String e) is optional — bare catch { } works too.finally is optional.Runtime errors and uncaught exceptions print to stderr with the line number, the offending source line, and a caret (^) pointing to the exact token:
KonsolScript Error (line 5): undefined variable 'total'
5 | Konsol:Print(total);
| ^^^^^
Uncaught throw exceptions show the same context, plus a full stack trace when the exception propagated through one or more function calls:
Uncaught exception (line 12): something went wrong
12 | throw "something went wrong";
Stack trace:
at inner (called from line 8)
at outer (called from line 3)
at main
The trace lists frames innermost-first. Each frame names the function and the line in the caller where the call was made.
Calling a method on a class that was never registered — whether a plugin that wasn't loaded or a .ks file that wasn't included — raises a clear error rather than silently doing nothing:
Runtime error at line 3: unknown class 'Sample' — missing #include?
All stdlib calls use the Class:Method(args) syntax. Methods that return a value follow the ByRef convention: the caller pre-declares a receiver variable and passes it as the last argument.
Var:Number h;
Time:GetHour(h);
Konsol:Print(h); // current hour
All stdlib modules follow the ByRef convention — there are no _ret exceptions.
| Call | Effect |
|---|---|
Konsol:Print(expr) |
Print one expression, then newline. Use + or ${} to combine values. |
Konsol:Message(expr) |
Alias for Print |
Konsol:Log(expr) |
Print with [LOG] prefix |
Konsol:Input(varName) |
Read a line from stdin into varName |
Konsol:Input("prompt", varName) |
Print prompt then read |
Konsol:Exit() |
Exit with code 0 |
Konsol:Exit(code) |
Exit with given code |
Konsol:Chr(n, outVar) |
ASCII char for code n → outVar |
Konsol:Asc(str, outVar) |
ASCII code of first char of str → outVar |
Konsol:IsNumeric(expr, outVar) |
true if value is a valid number → outVar |
Konsol:Delay(ms) |
Sleep for ms milliseconds |
Konsol:Run(cmd, outVar) |
Run shell command, capture stdout → outVar |
Konsol:Input stores a Number if the input looks like one, otherwise a String.
Passing more than one argument to Print, Message, or Log is a syntax error.
All methods are ByRef — pre-declare a receiver variable and pass it as the last argument.
| Call | Result |
|---|---|
Math:Abs(n, out) |
Absolute value |
Math:Round(n, out) |
Round to nearest integer |
Math:Floor(n, out) |
Floor |
Math:Ceil(n, out) |
Ceiling |
Math:Sqrt(n, out) |
Square root |
Math:Sin(n, out) |
Sine (radians) |
Math:Cos(n, out) |
Cosine (radians) |
Math:Tan(n, out) |
Tangent (radians) |
Math:Log(n, out) |
Natural logarithm |
Math:Random(out) |
Random float in [0, 1) |
Math:Random(max, out) |
Random float in [0, max) |
All methods are ByRef — pre-declare a receiver variable and pass it as the last argument.
| Call | Result |
|---|---|
String:Len(s, out) |
Length of s |
String:Left(s, n, out) |
First n characters |
String:Right(s, n, out) |
Last n characters |
String:Mid(s, start, len, out) |
Substring; start is 1-based |
String:Replace(s, from, to, out) |
Replace all occurrences of from with to |
String:Trim(s, out) |
Strip leading/trailing whitespace |
String:Upper(s, out) |
Uppercase |
String:Lower(s, out) |
Lowercase |
String:Reverse(s, out) |
Reverse characters |
String:Compare(a, b, out) |
-1 / 0 / 1 lexicographic |
String:Contains(s, sub, out) |
true if sub found in s |
String:Split(s, delim, out) |
Split; parts go to out, out1, out2, ...; count in outCount |
String:Join(delim, s1, s2, ..., out) |
Join strings with delimiter |
Var:String part;
String:Split("a,b,c", ",", part);
Konsol:Print(part); // a
Konsol:Print(part1); // b
Konsol:Print(part2); // c
Konsol:Print(partCount); // 3
File handles are integer IDs you choose (e.g. 1, 2).
| Call | Effect |
|---|---|
File:Open(handle, path, mode) |
Open file; mode is "r", "w", or "a" |
File:Close(handle) |
Close one file |
File:CloseAll() |
Close all open files |
File:Write(handle, expr) |
Write string to file |
File:Read(handle, outVar) |
Read next whitespace-delimited token → outVar |
File:ReadLine(handle, outVar) |
Read next line → outVar |
File:ReadAll(handle, outVar) |
Read entire remaining file content → outVar |
File:EOF(handle, outVar) |
true if at end of file → outVar |
File:Exists(path, outVar) |
true if file exists → outVar |
File:Delete(path, outVar) |
Delete file; true on success → outVar |
File:Open(1, "data.txt", "w");
File:Write(1, "hello\n");
File:Close(1);
Var:Boolean exists;
File:Exists("data.txt", exists);
Konsol:Print(exists); // true
// Read entire file at once
Var:String content;
File:Open(2, "data.txt", "r");
File:ReadAll(2, content);
File:Close(2);
Konsol:Print(content); // hello
// Read line by line
Var:Boolean eof;
Var:String line;
File:Open(3, "data.txt", "r");
File:EOF(3, eof);
while (eof == false) {
File:ReadLine(3, line);
Konsol:Print(line);
File:EOF(3, eof);
}
File:Close(3);
| Call | Effect |
|---|---|
Time:GetHour(outVar) |
Current hour (0–23) → outVar |
Time:GetMinute(outVar) |
Current minute → outVar |
Time:GetSecond(outVar) |
Current second → outVar |
Time:GetDay(outVar) |
Day of month → outVar |
Time:GetMonth(outVar) |
Month (1–12) → outVar |
Time:GetYear(outVar) |
Full year (e.g. 2025) → outVar |
Time:GetTimer(outVar) |
Seconds since process epoch (double, for timing) → outVar |
Var:Number h;
Var:Number m;
Var:Number s;
Time:GetHour(h);
Time:GetMinute(m);
Time:GetSecond(s);
Konsol:Print("${h}:${m}:${s}");
Var:Number t0;
Var:Number t1;
Time:GetTimer(t0);
// ... work ...
Time:GetTimer(t1);
Konsol:Print("elapsed: ${t1 - t0}s");
Dynamic resizable array with a declared element type. Results that return a value go to _ret.
| Call | Effect |
|---|---|
List:Number name |
Create an empty list of numbers |
List:String name |
Create an empty list of strings |
List:Boolean name |
Create an empty list of booleans |
List:ClassName name |
Create a list of class instances |
List:Push(name, val) |
Append a value |
List:Pop(name, outVar) |
Remove and return last element → outVar |
List:Get(name, index, outVar) |
Read element at 0-based index → outVar |
List:Set(name, index, val) |
Write element at 0-based index |
List:Size(name, outVar) |
Number of elements → outVar |
List:Remove(name, index) |
Remove element at index (shifts later elements) |
List:Contains(name, val, outVar) |
true if value found → outVar |
List:Clear(name) |
Remove all elements |
List:Sort(name) |
Sort in-place (numbers numerically, strings lexicographically) |
List:Sort(name, cmpFn) |
Sort in-place with comparator: cmpFn(a, b) returns Boolean |
List:Filter(src, predFn, dst) |
Fill dst with elements where predFn(x) is true |
List:Map(src, xfmFn, dst) |
Fill dst with xfmFn(x) applied to each element |
List:Reduce(src, redFn, init, outVar) |
Fold: redFn(acc, x) → final accumulator → outVar |
dst and outVar must be pre-declared. Filter/Map clear dst before filling it.
List:Number scores;
List:Push(scores, 10);
List:Push(scores, 20);
Var:Number sz;
List:Size(scores, sz);
Konsol:Print(sz); // 2
Var:Number val;
List:Get(scores, 0, val);
Konsol:Print(val); // 10
String-keyed dictionary.
| Call | Effect |
|---|---|
Map:New name |
Create an empty map |
Map:Set(name, key, val) |
Insert or overwrite a key |
Map:Get(name, key, outVar) |
Read value → outVar (0 if key absent) |
Map:Has(name, key, outVar) |
true if key exists → outVar |
Map:Remove(name, key) |
Delete a key |
Map:Size(name, outVar) |
Number of entries → outVar |
Map:Keys(name, listName) |
Populate a pre-declared List:String with all keys (order unspecified) |
Map:Clear(name) |
Remove all entries |
Map:New scores;
Map:Set(scores, "Alice", 95);
Map:Set(scores, "Bob", 82);
Var:Number val;
Map:Get(scores, "Alice", val);
Konsol:Print(val); // 95
Var:Boolean found;
Map:Has(scores, "Carol", found);
Konsol:Print(found); // false
Var:Number sz;
Map:Size(scores, sz);
Konsol:Print(sz); // 2
List:String keys;
Map:Keys(scores, keys);
List:Size(keys, sz);
Konsol:Print(sz); // 2
Parse, inspect, build, and serialize JSON. Documents are named and held in memory until freed. Paths use dot notation; numeric parts index arrays. Use "" for the root node.
| Call | Effect |
|---|---|
Json:NewObject name |
Create an empty {} document (no parens) |
Json:NewArray name |
Create an empty [] document (no parens) |
Json:Parse(name, str) |
Parse JSON string into named doc |
Json:Free(name) |
Release document |
Json:Stringify(name, path, outVar) |
Serialize doc or subtree → outVar |
Json:Get(name, path, outVar) |
Read value → outVar (typed: Number/String/Boolean; objects/arrays → JSON string) |
Json:Has(name, path, outVar) |
true if path exists → outVar |
Json:Type(name, path, outVar) |
"object" "array" "string" "number" "bool" "null" → outVar |
Json:Length(name, path, outVar) |
Element count for array or object → outVar |
Json:Keys(name, path, listName) |
Object keys → pre-declared List:String |
Json:Set(name, path, value) |
Write scalar at path (creates keys as needed) |
Json:Push(name, path, value) |
Append scalar to array at path ("" = root) |
Path examples: "name", "user.age", "items.0", "data.scores.2". Use "" for root.
Var:Number status;
Var:String name;
Var:String typeStr;
Var:Number len;
Var:String out;
// Parse and read
Json:Parse(resp, "{\"status\":200,\"user\":{\"name\":\"Alice\",\"scores\":[95,82]}}");
Json:Get(resp, "status", status);
Konsol:Print(status); // 200
Json:Get(resp, "user.name", name);
Konsol:Print(name); // Alice
Json:Type(resp, "user.scores", typeStr);
Konsol:Print(typeStr); // array
Json:Length(resp, "user.scores", len);
Konsol:Print(len); // 2
// Build and serialize
Json:NewObject cfg;
Json:Set(cfg, "host", "localhost");
Json:Set(cfg, "port", 8080);
Json:Stringify(cfg, "", out);
Konsol:Print(out); // {"host":"localhost","port":8080}
// Build array
Json:NewArray tags;
Json:Push(tags, "", "fast");
Json:Push(tags, "", "safe");
Json:Stringify(tags, "", out);
Konsol:Print(out); // ["fast","safe"]
Json:Free(resp);
Json:Free(cfg);
Json:Free(tags);
Pure string-based path manipulation plus filesystem queries.
| Call | Effect |
|---|---|
Path:Join(a, b, outVar) |
Joined path with native separator → outVar |
Path:Dirname(p, outVar) |
Parent directory ("." if none) → outVar |
Path:Basename(p, outVar) |
Filename including extension → outVar |
Path:Stem(p, outVar) |
Filename without extension → outVar |
Path:Extension(p, outVar) |
Extension with dot, e.g. ".ks" → outVar |
Path:Exists(p, outVar) |
true if path exists → outVar |
Path:IsFile(p, outVar) |
true if regular file → outVar |
Path:IsDir(p, outVar) |
true if directory → outVar |
Path:Absolute(p, outVar) |
Absolute path string → outVar |
Var:String result;
Path:Join("src", "main.ks", result);
Konsol:Print(result); // src/main.ks (or src\main.ks on Windows)
Path:Extension("readme.md", result);
Konsol:Print(result); // .md
Var:Boolean exists;
Path:Exists("script.ks", exists);
Konsol:Print(exists); // true
Operating-system services: working directory, directory listing, environment variables, and process control.
| Call | Effect |
|---|---|
OS:Cwd(outVar) |
Current working directory → outVar |
OS:Chdir(p) |
Change working directory — |
OS:Listdir(p, listName) |
Populate a pre-declared List:String with filenames (sorted) |
OS:Mkdir(p, outVar) |
Create directory; true if newly created → outVar |
OS:Mkdirs(p, outVar) |
Create directory tree; true if any dir was created → outVar |
OS:Remove(p, outVar) |
Remove file or empty dir; true if removed → outVar |
OS:Rename(old, new) |
Rename/move a file or directory — |
OS:Getenv(name, outVar) |
Environment variable value ("" if not set) → outVar |
OS:Setenv(name, value) |
Set environment variable — |
OS:System(cmd, outVar) |
Run shell command; exit code → outVar |
OS:Exit(code) |
Terminate the process — |
Var:String cwd;
OS:Cwd(cwd);
Konsol:Print(cwd); // /home/user/project (or C:\… on Windows)
Var:String greeting;
OS:Setenv("GREETING", "hi");
OS:Getenv("GREETING", greeting);
Konsol:Print(greeting); // hi
Var:Boolean ok;
OS:Mkdir("output", ok);
List:String entries;
OS:Listdir(".", entries);
Var:Number sz;
List:Size(entries, sz);
Konsol:Print(sz); // number of entries in current directory
Modules are registered as class handlers — the same mechanism the built-in classes use. You do not need to modify the parser.
kse_mymodule.cpp#include "kse.hpp"
static size_t mymodule_handler(Engine& e, size_t i) {
++i; // skip "MyModule"
if (e.tokenText(i) != ":") return e.skipOptSemi(i);
++i; // skip ":"
std::string method = e.tokenText(i);
++i; // skip method name
if (e.tokenText(i) == "(") ++i; // skip (
if (method == "Hello") {
// read input argument, then outVar (ByRef convention)
Value v = e.evalExpr(i);
if (e.tokenText(i) == ",") ++i; // skip comma
std::string outVar = e.tokenText(i); ++i; // read receiver name
if (e.tokenText(i) == ")") ++i; // skip )
std::string msg = "Hello from MyModule: " + toString(v);
e.setVar(outVar, Value(msg));
return e.skipOptSemi(i);
}
return e.skipOptSemi(i);
}
void register_mymodule(Engine& e) {
e.registerClass("MyModule", mymodule_handler);
}
kse.cppDeclare the registrar at the top:
void register_mymodule(Engine&);
Then call it in the Engine constructor:
Engine::Engine() {
scopedVars_[""];
register_math(*this);
// ... existing registrations ...
register_mymodule(*this); // add this line
}
MakefileSRCS = kse.cpp kse_konsol.cpp kse_math.cpp kse_string.cpp \
kse_file.cpp kse_time.cpp kse_class.cpp kse_try.cpp \
kse_mymodule.cpp
Var:String result;
MyModule:Hello("world", result);
Konsol:Print(result);
e.tokenText(i) // text of token at i
e.tokenCmd(i) // command/type id of token at i
e.tokenLine(i) // source line number
e.tokenCount() // total token count
e.evalExpr(i) // evaluate expression starting at i; advances i past it
e.setVar("name", v) // set a variable (use the caller's outVar name for return values)
e.getVar("name") // get a variable's Value
e.hasVar("name") // check existence
e.skipOptSemi(i) // call as final return; skips ; if present
e.skipPast("}", i) // scan forward to given token, return its index
// Value helpers (free functions from kse_types.hpp)
toString(v) // Value → std::string
toDouble(v) // Value → double
toBool(v) // Value → bool
Value("text") // make a string Value
Value(3.14) // make a number Value
Value(true) // make a bool Value
if (method == "Add") {
Value a = e.evalExpr(i);
if (e.tokenText(i) == ",") ++i;
Value b = e.evalExpr(i);
if (e.tokenText(i) == ",") ++i;
std::string outVar = e.tokenText(i); ++i; // receiver
if (e.tokenText(i) == ")") ++i;
e.setVar(outVar, Value(toDouble(a) + toDouble(b)));
return e.skipOptSemi(i);
}
The pattern: eval each input argument (leaving i at the following delimiter), skip commas between arguments, read the last token as the receiver variable name, skip ), write the result with e.setVar(outVar, ...).
If you want script code to instantiate your module type like a class (field storage, method dispatch), register it via Class:Create at runtime from a .ks file, or directly manipulate e.classDefs() and e.classInsts() from C++ — the same maps the built-in Class: keyword uses.
Plugins are shared libraries (.dll on Windows, .so on Linux/macOS) that extend KonsolScript with new module-style classes callable with the same MyPlugin:Method(args) syntax as built-in modules. No recompile of minks is needed.
The recommended way — load plugins directly in the script that uses them:
#include "kse_mysql" // finds kse_mysql.dll/.so in the plugin search path
#include "kse_audio"
See the #include section for the full search-path order.
minks install <name> install plugin from current directory
minks remove <name> delete it from the user plugin dir
minks list list all installed plugins
minks install searches the current working directory for / .so / .dylib, then lib / .so / .dylib. Run it from inside the directory where you built the plugin.
Installed plugins live in ~/.minks/plugins/ (Linux/macOS) or %APPDATA%\minks\plugins\ (Windows) and are available to all scripts without any flags.
cd sample_plugin
minks install kse_sample
minks list
# kse_sample
minks --plugin-path ./my_libs script.ks
--plugin-path can be repeated. Added dirs are checked before the defaults.
minks --plugin ./myplugin.dll script.ks
minks --plugin lib1.dll --plugin lib2.dll script.ks
minks -p ./kse_mysql.so -p ./kse_audio.so game.ks
--plugin (or -p) can be repeated. Libraries are loaded in the order given before the script runs. Prefer #include "name" in scripts over this flag for portability.
PluginClass builderInclude minks_plugin.h (ships with minks). The PluginClass RAII fluent builder handles all token-stream parsing internally; you only write lambdas for each method.
#include "minks_plugin.h"
#include <iostream>
extern "C" void minks_register(Engine& e) {
PluginClass(e, "MyLib")
// .byRefMethod — last script arg is the outVar; SDK writes result there.
// args[] inside the lambda contains only the INPUT arguments.
.byRefMethod("Greet", [](auto args) -> Value {
return Value(std::string("Hello, ") + toString(args[0]) + "!");
})
// .voidMethod — side-effects only; no receiver variable needed
.voidMethod("Log", [](auto args) {
std::cout << toString(args[0]) << "\n";
})
.byRefMethod("Add", [](auto args) -> Value {
return Value(toDouble(args[0]) + toDouble(args[1]));
});
// PluginClass destructor fires here, registering "MyLib" with the engine.
}
From KonsolScript:
Var:String greeting;
MyLib:Greet("world", greeting);
Konsol:Print(greeting); // Hello, world!
MyLib:Log("debug info"); // void — no receiver needed
Var:Number sum;
MyLib:Add(3, 7, sum);
Konsol:Print(sum); // 10
| Builder call | Output convention | Description |
|---|---|---|
.byRefMethod("Name", fn) |
Last script arg is the outVar | fn receives input args; its return Value is written to the caller's pre-declared variable |
.voidMethod("Name", fn) |
None | fn returns nothing; no variable is written |
Plugins have no _ret path. Every non-void plugin method must use .byRefMethod — there is no legacy method() builder.
Inside your lambda, args is std::vector. Use these free helpers (from kse_types.hpp):
| Helper | Type extracted |
|---|---|
toDouble(args[N]) |
Number (double) |
toString(args[N]) |
String |
toBool(args[N]) |
Boolean |
Return values: Value(double), Value(std::string), Value(bool).
# Windows
g++ -std=c++17 -shared -I<minks_dir> -o myplugin.dll myplugin.cpp
# Linux / macOS
g++ -std=c++17 -shared -fPIC -I<minks_dir> -o myplugin.so myplugin.cpp
Link additional libraries after myplugin.cpp:
g++ -std=c++17 -shared -fPIC -I../minks -o kse_mysql.so kse_mysql.cpp -lmariadb
is the directory containing minks_plugin.h, kse.hpp, and kse_types.hpp.
sample_plugin/kse_sample.cpp exposes a Sample class with six byRefMethod entries and one voidMethod. Build, install, and use:
# from sample_plugin/
make
minks install kse_sample # searches cwd for kse_sample.dll/.so
# In your .ks script:
#include "kse_sample"
Or for a quick test without installing (explicit load by path):
minks --plugin sample_plugin\kse_sample.dll tests\20_plugin.ks # Windows
minks --plugin ./sample_plugin/kse_sample.so tests/20_plugin.ks # Linux
Expected output (abridged):
=== Sample Plugin ===
plugin version: kse_sample 1.0.0
[Sample] Hello, world!
Add(3, 4) = 7
Multiply(6, 7) = 42
(10+20)*2 = 60
Upper = HELLO FROM PLUGIN
Repeat(ab, 5) = ababababab
--- IsPrime ---
2 is prime
3 is prime
...
minks_plugin.hEditors using clangd may flag 'kse.hpp' file not found or 'minks_plugin.h' file not found inside sample_app/ or sample_plugin/ source files. This is a false positive — clangd does not read those directories' Makefile and therefore does not pick up the -I.. flag. The make build compiles correctly. Add a compile_commands.json or a .clangd config with CompileFlags: Add: [-I..] to suppress it.
libkonsolscript is the interpreter as a shared library. Any C++17 application can link against it to make itself scriptable — no dependency on the minks CLI.
libkonsolscript.dll (Windows)
libkonsolscript.so (Linux)
libkonsolscript.dylib (macOS)
libkonsolscript.a (all platforms — static, built with `make static`)
The public API surface is two headers:
| Header | Contents |
|---|---|
kse.hpp |
Engine class — create, configure, load, run, query |
kse_types.hpp |
Value variant, Token, ClassDef, collection types, toString(), toDouble() |
Makefile (Windows/MinGW):
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks
sample_app.exe: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp -L/path/to/minks -lkonsolscript
# Copy libkonsolscript.dll next to the exe so Windows can find it
cmd /C copy /Y /path/to/minks/libkonsolscript.dll .
Makefile (Linux):
CXX = g++
CXXFLAGS = -std=c++17 -I/path/to/minks
sample_app: main.cpp
$(CXX) $(CXXFLAGS) -o $@ main.cpp \
-L/path/to/minks -lkonsolscript -Wl,-rpath,'$$ORIGIN'
# rpath=$ORIGIN means libkonsolscript.so is found next to the binary
macOS: replace -Wl,-rpath,'$$ORIGIN' with -Wl,-rpath,@loader_path.
#include "kse.hpp"
Engine engine; // ctor registers all stdlib modules
engine.setDebug(true); // optional: trace each statement to stderr
The Engine constructor registers all built-in modules (Konsol, Math, String, File, Time, List, Map, Json, OS, Hash, Date, CSV). No extra initialisation is needed.
Inline / REPL-style — append a source fragment and execute immediately:
engine.eval(R"(
Var:Number x = 6 * 7;
Konsol:Print(x);
)");
eval() preserves engine state between calls, so variables declared in one call are visible in the next. This is the same mode the minks REPL uses.
File-style — parse a complete script, then run it:
std::ifstream f("game.ks");
std::ostringstream ss; ss << f.rdbuf();
engine.loadScript(ss.str(),
"/path/to/scripts", // base dir for #include resolution
"game.ks"); // filename shown in error messages
engine.run();
Host → script (set a global before running the script):
engine.setVar("player_name", Value(std::string("Alice")));
engine.setVar("player_score", Value(0.0));
engine.setVar("debug_mode", Value(true));
Script → host (read a global after run() or eval() returns):
Value score = engine.getVar("player_score");
double s = toDouble(score);
Value status = engine.getVar("status");
std::string st = toString(status);
getVar returns a zero-value (0, "", false) for names that do not exist — same semantics as KonsolScript's zero-value design.
ValueValue(42.0) // Number
Value(std::string("hello")) // String — always use std::string, not const char*
Value(true) // Boolean
Value result = engine.callFunction("add", { Value(3.0), Value(4.0) });
double n = toDouble(result); // 7
The function must be defined in a script that was already loaded/eval'd. Missing functions throw a std::runtime_error.
Host-side classes use the same handler mechanism as built-in modules. The handler receives the raw token stream and must advance past the full statement.
engine.registerClass("Game", [](Engine& e, size_t i) -> size_t {
++i; // skip "Game"
if (e.tokenText(i) != ":") return e.skipOptSemi(i);
++i; // skip ":"
std::string method = e.tokenText(i++); // read method name
if (e.tokenText(i) == "(") ++i; // skip (
if (method == "GetScore") {
std::string outVar = e.tokenText(i++);
if (e.tokenText(i) == ")") ++i;
e.setVar(outVar, Value(current_score)); // ByRef pattern
}
return e.skipOptSemi(i);
});
For simpler class registration without token parsing, use the PluginClass builder from minks_plugin.h (see Plugin system
When a script uses #include "foobar", the engine searches its plugin path list. The host can prepend directories to this list before running any script:
engine.addPluginPath("/opt/mygame/plugins");
For iOS / embedded targets where dlopen is not available, register plugins statically before running the script:
// foobar_plugin.cpp exports: extern "C" void minks_register(Engine&);
engine.addPluginPreload("foobar", foobar_plugin_init);
// Now #include "foobar" in a script calls foobar_plugin_init instead of dlopen
Compile with -DMINKS_NO_DYNLOAD to strip all dynamic-loading code from libkonsolscript entirely.
sample_appsample_app/ in the minks repository is a minimal host application that demonstrates the complete embedding workflow. It has no dependency on the minks CLI source.
sample_app/main.cpp (abridged):
#include "kse.hpp"
#include <fstream>
#include <sstream>
int main(int argc, char* argv[]) {
Engine engine;
// Host → script: expose app metadata as globals
engine.setVar("app_name", Value(std::string("SampleApp")));
engine.setVar("app_version", Value(1.0));
if (argc >= 2) {
// File mode: load and run a .ks script
std::ifstream f(argv[1]);
std::ostringstream ss; ss << f.rdbuf();
std::string path = argv[1];
size_t sep = path.find_last_of("/\\");
std::string dir = (sep != std::string::npos) ? path.substr(0, sep) : ".";
engine.loadScript(ss.str(), dir, path);
return engine.run() ? 0 : 1;
}
// Inline mode: eval fragments, read results back
engine.eval("Var:Number result = 0; for (Var:Number i=1;i<=10;i++){result=result+i;}");
double sum = toDouble(engine.getVar("result")); // 55
return 0;
}
sample_app/demo.ks — the matching script:
Var:String banner = "Running inside: " + app_name + " v" + app_version;
Konsol:Print(banner);
Var:Number sq = 0;
Math:Sqrt(144, sq);
Konsol:Print(sq); // 12
Build and run:
cd sample_app
make
./sample_app # inline demo
./sample_app demo.ks # file mode
| Platform | Shared lib | Notes |
|---|---|---|
| Windows | libkonsolscript.dll |
make install places it in BINDIR alongside minks.exe; for embedding, copy it next to your host exe (no rpath on Windows) |
| Linux | libkonsolscript.so |
Link with -Wl,-rpath,'$$ORIGIN' so the .so is found next to the exe |
| macOS | libkonsolscript.dylib |
Link with -Wl,-rpath,@loader_path |
| iOS / embedded | static libkonsolscript.a |
Build with -DMINKS_NO_DYNLOAD; use addPluginPreload for plugins |
0.0/""/false.#include "file.ks" is fully supported with transitive includes and include-guard deduplication. See Preprocessor.main() are properly scoped and cleaned up after main returns (fixed).Math:Random uses srand/rand seeded once; not cryptographically secure.String:Mid uses 1-based start index (like BASIC), not 0-based.