Skip to content

Language Structure

Comming from other languages, the structure of cksp may feel unusual at first, especially since it is build on top of KSP, keeping some of the unusual aspects of its syntax and execution model while adding features from modern languages. That is why this chapter is designed to give you a quick overview of the language structure and syntax, covering the core constructs of CKSP, including callbacks, variables, functions, control flow, custom data structures, imports, namespaces, and pragma directives.

Callbacks and Event-Driven Programming

As CKSP is based on KSP, it is an event-driven language. A script does not start in a main function, but reacts to Kontakt events through callbacks. The primary entry point is usually on init, which runs once after compilation or engine restart and is typically used for declarations, UI setup, and initial state. All other callback types are triggered by specific events such as incoming MIDI data, UI interaction, timers, or asynchronous completion signals.

Minimal CKSP Script Structure
1
2
3
4
5
6
7
8
on init
    declare ui_knob knb_volume (0, 1000000, 1)
    set_knob_label(knb_volume, "Volume")
end on

on ui_control (knb_volume)
    set_engine_par(ENGINE_PAR_VOLUME, knb_volume, -1, -1, -1)
end on

Syntax Fundamentals

Like KSP, cksp is line-oriented. In practice, one statement is written per line and semicolons are not required. Indentation is used for readability, not for defining scope. Blocks are opened with a keyword and closed with end <keyword>, for example if ... end if, while ... end while, or on ... end on.

Assignments use :=, equality checks use =, and inequality checks use #. User-defined names must be declared before use. Multiline formatting is supported for function calls and initializers, which allows code to stay readable in larger expressions.

my_function(
    arg1,
    arg2,
    arg3
)

If a statement or expression must continue on the next line, use the continuation marker <...> at the end of the current line.

Execution Model

For each incoming event, Kontakt first sets the relevant built-ins for the active context, such as $EVENT_NOTE, $EVENT_VELOCITY, or $NI_UI_ID, and then invokes the matching callback. A callback runs sequentially until it reaches end on, exits early via exit, or pauses on wait().

The wait() case is essential for understanding runtime behavior. While one callback instance is paused, other callback instances may run. This is why scope rules and thread-safe local declarations matter, especially in combination with #pragma max_callback_depth(...).

Core Language Constructs

Callbacks

Callbacks define the top-level structure of a script and always follow the on <callback_name> ... end on pattern. Most projects begin with on init and then add only the callback types they need, for example on note, on release, on controller, on ui_controls, on ui_control(<widget>), on persistence_changed, on listener, and on async_complete.

exit can be used to stop a callback early. stop_wait(<callback_id>, <parameter>) can be used to cancel a pending wait.

Variables, Arrays, and Declarations

CKSP provides primitive types such as int, real, string, and bool, as well as composite types such as arrays and multidimensional arrays (int[], real[][], ...). Pointer variables for user-defined structures are supported as well. Global state is commonly declared in on init, while local state can be introduced directly in the function, loop, or callback scope where it is needed.

Immutable values can be declared with const. In larger scripts, separating declaration, assignment, and scope decisions early helps keep data flow and state lifetime predictable.

  • Variables and Arrays


    Types, naming rules, arrays, multidimensional arrays, and indexing behavior.

    Variables and Arrays

  • Declarations


    Declaration syntax, type annotations, constants, and persistent declarations.

    Declarations

  • Assignments


    Assignment forms for variables, arrays, multidimensional arrays, and compound operators.

    Assignments

  • Lexical Scope


    Local/global scope rules and thread-safety behavior in asynchronous callbacks.

    Lexical Scope

Functions

Functions are defined in global scope and can be called from callbacks or other functions. Return values are produced via return. Parameter passing is by value by default. Reference semantics can be enabled per parameter with ref, or project-wide through #pragma pass_by("reference").

function <name>(<params>): <type>
    ...
    return <value>
end function
  • Functions


    Function signatures, return values, parameter passing, and ref semantics.

    Functions

Control Flow and Loops

Branching is expressed with if / else if / else, select / case / default, and ternary expressions (condition ? a : b). Iteration is expressed with for, for ... in, and while. Loop flow can be controlled using break and continue.

  • Control Flow


    Conditional expressions and branching with if, select, and ternary syntax.

    Control Flow

  • Loops


    for, for-each, and while loops, including break and continue.

    Loops

Custom Data Structures

struct allows defining custom data types with fields and methods. They are placed in the global scope, outside of callbacks or functions. While not as fully featured as classes in object-oriented languages, they provide a way to group related data and behavior together, which is especially useful for more complex projects. Instances are handled through pointer semantics and can be created with new. Constructors (__init__) and, experimentally, operator overloading are supported.

struct Note
    declare pitch: int
    declare velocity: int

    function play(self): int
        return play_note(self.pitch, self.velocity, 0, -1)
    end function
end struct
  • Custom Data Structures


    Structs, methods, constructors, pointer semantics, and experimental overloading.

    Custom Data Structures

Imports and Namespaces

For modular code organization, files can be included with import. Paths can be resolved relative to the current file or to the project root. Namespaces organize global declarations and reduce naming collisions in larger projects.

import "path/to/module.cksp"

namespace Math
    declare const PI := 3.14159
end namespace
  • Import Statements


    Path resolution, root-relative imports, and import usage inside constructs.

    Import Statements

  • Namespaces


    Name organization, nested namespaces, lookup order, and shadowing behavior.

    Namespaces

Pragma Directives

Pragmas define project-wide compiler behavior directly in source files. In practice, the most relevant directives are #pragma output_path(...), #pragma optimize(...), #pragma pass_by(...), #pragma combine_callbacks(...), and #pragma max_callback_depth(...).

  • Pragma Directives


    Project-wide compiler directives for output, optimization, callback handling, and pass-by settings.

    Pragma Directives

Fast Start Guidance

A practical way to start is to define shared state and UI in on init, implement only the callbacks needed for the current behavior, and move reusable logic into global functions early. Prefer local declarations to avoid cross-instance side effects. As a script grows, import, namespace and struct are the main tools for keeping structure maintainable.