Lexical Scope
Scopes and Local Variables
In CKSP, variables can be declared locally within any scope.
They retain their value as long as the program flow remains within that scope and are no longer accessible once the scope ends.
This ensures that locally declared variables cannot be accessed from outside, limiting their lifetime and preventing unintended side effects.
In contrast, global variables can be referenced in any scope unless they are shadowed by a local variable within that scope.
By default, all variables declared in the on init callback are global.
Variable Types
Local declarations are not limited to scalars, they can also be used for arrays or multidimensional arrays. The syntax for declaring arrays and multidimensional arrays is identical to the syntax for scalar variables.
Lexical Scoping for Clarity
CKSP introduces the concept of lexical (or static) scoping, a standard approach in many modern programming languages.
Lexical scoping allows a variable's visibility to be determined from the source code alone, even before program execution.
This makes it easy to see at a glance which variables belong to which part of the code.
For instance, if variables are only used within a specific callback, they can be declared locally within that callback, improving readability and organization, rather than cluttering the global on init section.
This allows for more expressive variable names, as seen in the examples below. The compiler internally transforms all local variables into global KSP variables at compile time, reusing them where possible to keep memory usage low.
Local Variables in Loops
The following example demonstrates the use of local variables within loops. Each iteration of the loop can have its own local variables, which are inaccessible outside of the loop.
Within the loop scope, a new variable j is declared, independent of the j defined outside, resulting in the inner variable always having the value 0. This is an example of shadowing, where a variable with the same name in an inner scope overwrites a variable in the outer scope without affecting the outer variable.
Local Variables in Control Flow Statements
The same principle applies to variables or arrays in if-statements and other language constructs. In the example below, a variable is declared in both the if and else blocks, so each scope contains a variable j with a different value. If no global variable j was declared in the on init callback, attempting to access j after the scope of the if-statement results in a compiler error.
Denoting Global and Local Declarations
By default, all variables declared in the on init callback are global, whereas in other callbacks, they will be local to that callback.
You can go around this by using the global keyword to explicitly declare a variable as global, even within a local scope, or by using the local keyword to enforce local declaration within an otherwise global scope.
What is more, variable declarations can also be made outside of any callback, in which case they will be in the global scoped (and moved to the beginning of the on init callback by the compiler).
Global Declarations in Local Scopes
Declaring a variable as global inside a function or local scope will cause the compiler to lift it into the global scope along with its assignments.
If these assignments rely on local variables that are out of scope after the lift, it may lead to compile-time errors or undefined behaviour.
Thread-Safety
Internally, all CKSP variables (local and global) are ultimately transformed into global KSP variables.
To reduce memory usage, the compiler uses different techniques to recycle variables once they go out of scope. This has implications for the thread-safety of variable access.
In the normal sequential execution model, KSP ensures that each callback runs to completion before the next one starts, preserving variable values for the duration of that callback.
---
config:
layout: elk
look: handDrawn
theme: neutral
---
graph LR
subgraph callback A
A(on note) -- code logic --> B(end on)
end
B -. next cb .-> C
subgraph callback B
C(on release) -- code logic --> D(end on)
end
D -. next cb .-> E
subgraph callback C
E(on ui_controls) -- code logic --> F(end on)
end
However, when asynchronous operations such as wait() or wait_async() are used, callbacks may be interrupted.
KSP can pause the current callback and run another one before resuming, potentially overwriting variables.
This can cause problems, as the variables used in both callbacks may no longer have the expected values. A variable might have a different value at the end of a callback than it had at the beginning.
---
config:
layout: elk
look: handDrawn
theme: neutral
---
graph LR
subgraph callback A
A(on note) -- code logic ---> B{wait}
B -- code logic --> C(end on)
end
C -. next cb .-> F
subgraph callback C
F(on release) -- code logic ---> G(end on)
end
B -. next cb .-> D
subgraph callback B
D(on ui_controls) -- code logic --> E(end on)
end
E -. continue cb .-> B
To prevent this issue for locally declared variables, the compiler employs two primary strategies:
- Restricted Variable Reuse: Locally declared variables in thread-unsafe callbacks (callbacks containing or calling asynchronous operations) are only reused in other thread-unsafe callbacks where Dimension Expansion is employed. This helps prevent variables used in one callback instance from being unexpectedly overwritten by another callback instance before the first completes.
- Dimension Expansion: To maintain variable consistency when a callback is called before the previous one completes its execution, the compiler extends all locally declared variables within a thread-unsafe callback by an additional dimension, effectively transforming them into arrays. Each concurrent execution of the callback then uses a different index in this array, based on a unique callback ID (
NI_CALLBACK_ID). This ensures that each "instance" of the callback execution operates on its own set of variable values, guaranteeing consistency.
Thread Safety Warning and Limitations
These protections only apply to locally declared variables.
Global variables — including those declared in on init — are not thread-safe and may be overwritten if accessed from multiple asynchronous callbacks.
For example, a global pointer assigned in a thread-unsafe callback could later point to the wrong memory location.
This feature may yield unpredictable results under extreme concurrent callback loads.