KonsolScript

KonsolScript is modern lightweight embeddable scripting language optimized for tooling, games, automation, and beginner accessibility designed for someone who values clarity, portability, and extensibility.


Table of Contents

  1. Language basics
  2. Operators
  3. Control flow — if/else, while, for, foreach, break, continue, switch/case
  4. Functions
  5. Arrays
  6. Structs (UDTs)
  7. Classes
  8. Error handling
  9. Built-in modules — Konsol, Math, String, File, Time, List, Map, Json
  10. Adding a module
  11. Plugin system
  12. Embedding API

Language basics

Types

Three primitive types, all stored in a Value variant:

Type Declaration keyword Default
Number Var:Number 0
String Var:String ""
Boolean Var:Boolean false

Variable declaration

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";

Literals

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

String interpolation

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

Guru-level: full expressions inside ${}

${} 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}");

Comments

// single-line comment

/* multi-line
   comment */

Preprocessor

#define

Plain word-boundary text substitution before tokenisation:

#define MAX 100
#define PI 3.14159

Var:Number limit = MAX;

#include

Two 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"

Plugin includes

#include "kse_sample"     // loads kse_sample.dll / .so from plugin search paths

Plugin search order for #include "name":

  1. /plugins/name.dll
  2. /plugins/name.dll
  3. Dirs added via engine.addPluginPath() or the --plugin-path CLI flag
  4. MINKS_PLUGIN_PATH env var (colon-separated on Linux/macOS; semicolon on Windows)
  5. ~/.minks/plugins/ (Linux/macOS) — %APPDATA%\minks\plugins\ (Windows)
  6. ./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

Entry point

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.


Operators

Arithmetic

Op Meaning
+ Add / concat
- Subtract
* Multiply
/ Divide
% Modulo
^ Power

+ on a string and any value concatenates both as strings.

Comparison

== != > >= < <=

Logical

&& || !

Assignment

= += -= *= /= %= ++ --

Precedence (high to low)

Unary (-, !) → ^* / %+ - → comparison → &&||


Control flow

if / else if / else

if (x > 10) {
    Konsol:Print("big");
} else if (x > 5) {
    Konsol:Print("medium");
} else {
    Konsol:Print("small");
}

while

Var:Number i = 0;
while (i < 5) {
    Konsol:Print(i);
    i++;
}

for

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.

break

while (true) {
    if (done == true) { break; }
}

continue

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
}

switch / case / default

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;
}

foreach

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
}

Functions

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.


Arrays

Fixed-size typed arrays. Size is set at declaration and does not change. Use List for dynamic resizable collections.

Declaration

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.

Access

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.

Iterating

for (Var:Number i = 0; i < 5; i++) {
    Konsol:Print(scores[i]);
}

foreach (s in scores) {
    Konsol:Print(s);
}

Note on performance

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.


Structs (UDTs)

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.


Classes

Full OOP with fields and methods.

Definition

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.

Instantiation and use

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.

Collections of class instances

List:Animal  pack;   // list of Animal instances
Array:New herd[10]:Animal;   // array of Animal instances

Multiple instances

Each Class:TypeName varName (or Var:TypeName varName) creates an independent instance with its own copy of the fields.


Error handling

try {
    throw "something went wrong";
} catch (String e) {
    Konsol:Print("Caught: " + e);
} finally {
    Konsol:Print("always runs");
}

Error output format

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?

Built-in modules

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.

Konsol

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 noutVar
Konsol:Asc(str, outVar) ASCII code of first char of stroutVar
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.

Math

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)

String

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

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);

Time

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");

List

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

Map

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

Json

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);

Path

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

OS

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

Adding a module

Modules are registered as class handlers — the same mechanism the built-in classes use. You do not need to modify the parser.

1. Create 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);
}

2. Register it in kse.cpp

Declare 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
}

3. Add to Makefile

SRCS = 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

4. Use it in a script

Var:String result;
MyModule:Hello("world", result);
Konsol:Print(result);

Engine API available to handlers

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

Multiple methods, multiple arguments

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, ...).

Exposing the module as a script-side class

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.


Plugin system

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.

Loading plugins from a script

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.

Managing installed plugins

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 .dll / .so / .dylib, then lib.dll / .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

Adding a search directory (CLI)

minks --plugin-path ./my_libs script.ks

--plugin-path can be repeated. Added dirs are checked before the defaults.

Low-level explicit load (legacy / debug)

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.

Writing a plugin — the PluginClass builder

Include 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 API

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.

Argument extraction

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).

Build commands

# 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.

Working example — sample plugin

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
...

IDE false-positive on minks_plugin.h

Editors 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.


Embedding API

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()

Linking

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.

Engine lifecycle

#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.

Running scripts

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();

Passing data between host and script

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.

Constructing Value

Value(42.0)                        // Number
Value(std::string("hello"))        // String  — always use std::string, not const char*
Value(true)                        // Boolean

Calling script functions from the host

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.

Registering a host class

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

Plugin search paths (embedding)

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.

Worked example: sample_app

sample_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 notes

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

Notes and known limits