Added Features - nojanath/SublimeKSP GitHub Wiki

New Extended Syntax

A set of new functions that make programming in KSP easier have been added to the compiler. These functions are part of the preprocessor, which occurs near the beginning of the compiling process. Syntax highlighting for these new commands has been included. These new additions will not negatively affect any of your current scripts. It is highly recommended that the 'Optimize compliled code' compiler option is turned on! It is also recommended that you only use the variable prefixes !%$@ when you have to.

Processing order of the preprocessor

This list isn't completely comprehensive, but it should give a good overview of the order in which the preprocessor performs it's functions. It is perhaps useful to have this knowledge so macros can be better utilised.

  • Firstly, code is imported from other files where the import command is used.
  • All comments are removed.
  • Lines with continuation ellipsis ... are joined together.
  • Code with USE_CODE_IF/IF_NOT is appropriately removed.
  • define macros are solved.
  • iterate_macro and literate_macro are executed, leaving a list of macros for further down the line.
  • Regular macros are substituted (the macro body is placed wherever the macro is called).
  • All other preprocessor functions (UI arrays, lists, open size arrays, constant blocks, concat, etc.).
  • Regular declare const variables are substituted into literals (if 'Optimize compliled code' is enabled).

Handy Syntax Improvements

Comments

Use // to start a comment. Unlike the default {} comments, these comments always finish at the end of a line. {} comments still work as they used to. Sublime hotkey Ctrl/ will now use //, and block comment hotkey CtrlShift/ will use {}.

Note that you should not use // comments within inlined array initializers, because it will not work! In this case, always use the default {} comments.

There is also a new variant for block comment, /* */, like in C/C++.

Number Types

SublimeKSP offers these ways to write numbers in hex and binary formats:

declare a
a := 0x77AAFF // sets a to 77AAFF, or 7842559
a := 077AAFFh // sets a to 77AAFF, or 7842559. First 0 can be a digit 0-9!
a := 1100b // LSB-right, sets a to 12
a := b1100 // LSB-left, sets a to 3

Less constrained variable initialization

You can now always assign a value to a variable on the same line you declare it, whereas before you could only do this with constants and integer variables initialized with literals or constants.

declare modID := find_mod(0, "lfo")
declare @string := "text"

String array improvements

You can now declare and initialize a string array on the same line, like you would an integer array. If you initialize it with one string only, the whole array will contain that same string.

declare !textArray[2] := ("string1", "string2") 
declare !textArray2[15] := ("initial text") // All 15 elements will be "initial text"

Automatic array size

When you declare an array and initialize its elements on the same line, it is now optional to include the array size. You can access a constant for the size later on in your script with arrayName.SIZE.

declare myArray[] := (79, 34, 22)
declare !strings[] := ("s1", "s2")
message(myArray.SIZE) // message(3)

Miscellaneous

Compiled code now contains a comment with the time and date it was compiled on.

Built-in defines were added that allow you to use time and date in various formats as strings directly in your code. They are:

__SEC__
__MIN__
__HOUR__
__HOUR12__
__AMPM__
__DAY__
__MONTH__
__YEAR__
__YEAR2__
__LOCALE_MONTH__
__LOCALE_MONTH_ABBR__
__LOCALE_DATE__
__LOCALE_TIME__

New Features

New Pragma Directives

It is now possible to force the script to compile with a particular compiler option enabled by writing { #pragma compile_with <name of compiler option> }, or disabled by writing { #pragma compile_without <name of compiler option> }. The available compiler options are as follows:

remove_whitespace
compact_variables
combine_callbacks
extra_syntax_checks
extra_branch_optimization
optimize_code (automatically forces extra_syntax_checks to be enabled!)
add_compile_date
sanitize_exit_command

The way this works in the back end is the compiler is first scanning for all compile_with pragmas and collects them, then scans for all compile_without pragmas, however if the same compiler option is found twice, only the first match will be taken into account. Effectively, this means that "with" pragmas take precedence over "without" ones, and that one should be very careful not to define multiple pragmas for the same compiler option!

It is now also possible to use multiple save_compiled_source pragmas, in order to save the compiled code to multiple files simultaneously.

Creator Tools GUI Designer support

Since Creator Tools 1.2 release that coincided with Kontakt version 6.2, Creator Tools contains a property-based GUI Designer tool to create performance views. There is a new file type to store these performance views, .nckp, which is JSON-based. SublimeKSP supports importing these .nckp files since version 1.9.0, so that the compiler can pull all UI control definitions from them, and proceed with compilation smoothly.

This is achieved with a new command, import_nckp(<path>). This command can be used in any callback (or even outside of them). Path can be absolute or relative. Multiple .nckp files can be imported and parsed. Note that this only parses the files, it doesn't actually load the performance views. For this, you still need to use load_performance_view() command (consult KSP reference).

Compiler will throw an error if the .nckp file doesn't exist at the specified path, and also if both make_perfview and load_performance_view() are found inside init callback, as this is not supported by Kontakt.

on init
	import_nckp("C:\Work\Project\Resources\performance_view\my_GUI.nckp")
	load_performance_view("my_GUI")
end on

Define macros

There is a new macro type called the 'define' macro. Defines are a way to do text substitutions at the very beginning of the compilation process. They are completely global and can be declared anywhere. They are not a part of your actual program, so it is usally best to declare them outside of any callbacks (though sometimes it can improve readablity to put them inside callbacks/functions).

Some important things to note about defines:

  • Defines will substitute anything - variable names, Kontakt functions, etc. Because of this, it is very important to use unique names for them. Using capital letters for the name is a good idea.
  • They are just simple text substitutions, they have no concept of the script's structure. If you create a define in a regular macro (or any location), it will still exist regardless of whether the macro is called or not.
  • If you assign a math expression to a define, wrap it in brackets. Defines will try and evaluate any math expressions, however if i.e. the expression contains a declare const it will not be able to, therefore brackets are needed in order for it to behave correctly.
  • Don't use symbol prefixes for defines !%$@.
define NUM := 20
define VALUE := (20 / 3)
define STRING := "text"
define FUNC(a, b) := (a + b)
define MENU_NAME(#name#) := #name#Menu

on init
    message(NUM) // message(20)
    message(FUNC(1, 2) * FUNC(3, 4)) // message((1 + 2) * (3 + 4))
    declare ui_menu MENU_NAME(sound) // declare ui_menu soundMenu
end on

Structs

Structs are a data structure for variables in Kontakt. They are used to group together a set of similar variables. While not as fully featured as structs you might find in other programming languages, they are useful nonetheless. They can be considered to be instances of families (families are already a part of SublimeKSP), rather than fully fledged structures. First, a struct is declared by using a struct block that starts with struct <name> and ends with end struct. Inside this block are variable declarations, and nothing else. Once this struct is created, instances of it can be declared in your script using the following syntax: declare &<StructName> <nameOfInstance>, where <StructName> is the name of the struct, and <nameOfInstance> is the name of the instance. The & symbol is required for the compiler to know that it's dealing with a data structure. Structs can also be declared as arrays or multidimensional arrays: declare &<StructName> <nameOfInstance>[20]. Structs can also have other structs as members - simply declare another struct inside the declaration block. Members of the struct are accessed with the dot operator, similarly to families. If the struct has other structs as members, these will be accessed with another dot operator, and so on.

Notes:

  • Arrays or multidimensional arrays of structs will have .SIZE constants generated.
  • As explained in the Multidimensional Arrays and UI Arrays sections, the raw single dimension version of an array can be accessed by prefixing the name with an underscore. For arrays of structs, the underscore of any members that are multidimensional will have the underscore at the beginning of the whole name, not just that dot-separated section.
  • It is possible to declare an instance of a struct in a function.
struct MyStruct
    declare var := 2
    declare array[2]
    declare ui_switch switch
end struct

struct Point
    declare x
    declare y
end struct

// Use a define macro to simulate passing the struct as an argument to a function.
define POINT(#pointName#) := #pointName#.x, #pointName#.y
function offsetPoint(x, y, amount)
    x := x + amount
    y := y + amount
end function

on init
    declare &MyStruct myStruct
    declare &MyStruct singleArray[2]
    declare &MyStruct multiArray[2, 3]
    message(singleArray.SIZE)
    message(multiArray.SIZE_D1)
    message(multiArray.SIZE_D2)
    singleArray[0].var := 20
    multiArray[0, 1].array[0] := 2
    singleArray[0].switch -> value := 0

    declare &Point myPoint
    offsetPoint(POINT(myPoint), 20)
end on

// UI controls in one-dimensional structs can be normally accessed like so:
on ui_control(singleArray.switch0)
    message("switch from singleArray")
end on

// Because ui_control callback does not accept UI IDs, we must access the raw version of the switch by using the underscore.
// Note the position of the underscore - at the front of the whole name, not after the dot!
on ui_control(_multiArray.switch0)
    message("switch from multiArray")
end on

Limitations:

  • You cannot initialize a struct member with a function, for example: declare var := find_zone("zone"). However these are fine: declare num := ENGINE_PAR_VOLUME or declare @text := "blah"
  • You cannot use the struct instance name in any operations, everything must be done by accessing the members. For example, you cannot try and assign one struct instance to another, each member will have to be assigned individually. (You could create functions that add all struct members if you wanted to.)
  • You cannot make a struct persistent, but this can be done individually on the members.
  • Array struct members cannot be initialized with a list: declare array[3] := (2, 3, 4) is not allowed. However, arrays can be initialised to a single value: declare array[3] := (2)
  • Lists as struct members will only work for single instances of structs (not arrays).
  • You cannot make arrays of structs which have declare constants.

Constant blocks

You can create a set of integer constants in a block. The value of the constants can optionally be automatic, whereas by default the first is 0 and each one after is incremented. There is also an integer array generated, though as usual if you do not use it, the compiler will remove it when 'Optimize compiled code' is enabled.

const colours
    WHITE := 9FFFFFFh
    BLACK := 9000000h
    RED   := 9FF0000h
    GREEN := 900FF00h
    BLUE  := 90000FFh
end const

message(colours.SIZE) // How many elements are in colours.
message(colours.BLACK)
message(colours[0]) 

const types
    SEQUENCER // Auto-generated 0
    LFO       // Auto-generated 1
    GRID      // Auto-generated 2
end const

declare read ui_slider slider(0, types.SIZE - 1)

if slider = types.SEQUENCER
    message(slider)
end if

Variable Persistence Shorthand

There are three new keywords that can be used when declaring a variable as a handy way of setting the persistence.

The first is pers which would be the same as writing make_persistent() in the next line. The second is read which is the same as writing make_persistent() and then read_persistent_var() in the next two lines. The third is instpers which is the same as writing make_instr_persistent() in the next line.

declare pers variable
declare pers array[10]
declare pers ui_switch mySwitch
declare read ui_slider sliders[20] (0, 100)
declare instpers selected_tab

Multidimensional Arrays

You can now create arrays with multiple dimensions. These can be either integers, strings or ui_controls. They will work with instpers/pers/read keywords. Behind the scenes, these arrays work by declaring a regular array and a property with get/set functions. If you want to access the raw array, it is the same name as the multidimensional version prefixed with an underscore: _array[0]. Each array also has built-in constants for the number of elements in each dimension. They follow this pattern: <array-name>.SIZE_D1 or <array-name>.SIZE_D2, etc. You can initialize a multidimensional array, this works in the same way as you would initialize a regular array, regardless of the number of dimensions.

declare array[2, 3] := (1, 4, 6, 3, 7, 7) // 2D array with its 6 elements initialised.
array[0, 1] := 100
message(array[0, 1])
message(array.SIZE_D1)
load_array(_array, 1) // When accessing the array as a whole, we use the underscored version
declare ui_slider sliders[2, 3] (1, 100) // A 2D array of sliders
declare !text[2, 2, 2] := ("text") // 3D array, all elements initialised to "text"

UI Arrays

UI arrays are simply duplicates of the declared UI control which can be accessed through an array. Use square brackets to state the number of elements. Regular KSP constants cannot be used here, only new define constants or literal numerical values are valid. For ui_tables, the first square bracket is for the number of elements in the array, the second is for the size of the ui_table. The UI ID of each can be accessed with arrayName[0], or if you need the actual variable name, it's arrayName0 (using 0 as an example here). UI arrays can also be multidimensional. In this case, to access the raw variable names or the single dimension version, you must prefix the name with an underscore. Note that there are no .SIZE constants generated for a multidimensional UI array. UI callbacks for a UI array can all be easily generated with a macro (see Iterate Macros section).

define NUM_CONTROLS := 40
declare pers ui_slider volumeSliders[NUM_CONTROLS] (0, 100)
declare ui_table tables[40] [100](2, 4, 100)
declare ui_switch switches[2, 2]
_switches0 -> value := 1 // Access the variable name of a multi-dim UI array with an underscore
define NUM_LAYERS := 4
define NUM_CONTROLS := 4

on init
    // Create all of the UI in one command. The total number of controls created 
    // is simply equal to NUM_LAYERS * NUM_CONTROLS.
    declare pers ui_slider sliders[NUM_LAYERS, NUM_CONTROLS] (0, 100)
    declare i
    declare j
    for i := 0 to NUM_LAYERS - 1
        for j := 0 to NUM_CONTROLS - 1
            set_bounds(sliders[i, j], 10 + 40 * i, 10)
        end for
    end for
end on

function hideAllSliders()
    // Here is an example of how you might want to use the single dimension version
    // to effect all the sliders at once.
    for i := 0 to num_elements(_sliders)
        _sliders[i] -> hide := HIDE_WHOLE_CONTROL
    end for
end function

macro sliderCallbacks(#n#)
    on ui_control(_sliders#n#) // The single dimension version has an underscore.
        message(_sliders#n#)
    end on
end macro

iterate_macro(sliderCallbacks) := 0 to NUM_LAYERS * NUM_CONTROLS - 1 

Function for Array Concatenation

There is a new function for concatenating any number of (regular, not multidimensional) arrays into one. When used in a declare statement, you can declare an array with an open size ([]) and then assign it a value using the concat function. The array size will be automatically calculated to equal the sum of the array sizes of the concatenated arrays. The concat function can also be used anywhere else in your code, just make sure the array is large enough, otherwise you will get an error in Kontakt. The concat function can take one or more parameters, the order in which you list them is the order in which the array will contain the values. If you just have one parameter in the concat function, it essentially makes one array equal to the other.

on init
    declare data1[] := (20, 30, 40, 60)
    declare data2[] := (40, 70, 50, 90)
    declare myArray[] := concat(data1, data2)
    declare pers ui_slider volumeSliders [2] (0, 100)
    declare pers ui_slider tuneSliders   [2] (0, 100)
    declare allSliderIDs[] := concat(volumeSliders, tuneSliders)
    declare const NUM_TABLE_COLUMNS := 100
    declare ui_table myTable[NUM_TABLE_COLUMNS] (2, 2, 1000)
    declare tableData[NUM_TABLE_COLUMNS]
end on

on ui_control(myTable)
    tableData := concat(myTable)
end on

List - New Array Type

Lists are a simple construct that allow you to append values to the end without having to specify the element index. Once declared, use the list_add() command to add values. They can only be used in the init callback, but not in loops or if statements. There is a constant for the size generated: listName.SIZE.

declare list controlIds[]
list_add(controlIds, get_ui_id(slider0))
list_add(controlIds, get_ui_id(slider1))

Instead of using a whole load of list_add() commands, you can declare a list and assign it a set of values in a code block. There is also a constant variable for the list size generated. You cannot use instpers, pers or read keywords for the list block.

list !menuItemText[]
    "Oscillator"
    "Filter"  
    "LFO"
end list

message(menuItemText[0])
message(menuItemText.SIZE)

list myList[]
    99
    24
    get_ui_id(volSlider)
    get_ui_id(tuneSlider)
    get_ui_id(panSlider)
end list

You can also create a list of lists (sometimes called jagged arrays). This is a 2D version of the list array type, in which you can add single dimension arrays (or regular variables) to the list as elements. The syntax is similar to regular lists, except you are required to leave open brackets with a comma in them [,]. You can then access the data in the regular 2D array fashion arrayName[0, 2]. Also, like with regular multidimensional arrays, if you need to access the raw single dimension version, you can prefix the name with an underscore. Because the size of each element can be different, it is important to be careful that you are accessing the correct elements. The size of each element is stored in a special array called arrayName.sizes. So, for example, to get the size of the first element it would be arrayName.sizes[0]. The number of elements can be retrieved with the constant arrayName.SIZE. Note that this is different from num_elements(arrayName). Lists of lists also can be written in a list block, to denote that it is a list of lists, open brackets with a comma are required. If the data can initialized on a declare array line (literals or constants), you can also just list raw data separated by commas in the list block.

on init
    declare filterParams [] := (ENGINE_PAR_CUTOFF, ENGINE_PAR_RESONANCE)
    declare reverbParams [] := (ENGINE_PAR_RV_SIZE, ENGINE_PAR_RV_DAMPING, ENGINE_PAR_RV_COLOUR)
    declare delayParams  [] := (ENGINE_PAR_DL_DAMPING, ENGINE_PAR_DL_TIME, ENGINE_PAR_DL_PAN, ENGINE_PAR_DL_FEEDBACK)
    declare wetParam := ENGINE_PAR_SEND_EFFECT_OUTPUT_GAIN

    declare list engineParams[,]
    list_add(engineParams, filterParams)
    list_add(engineParams, reverbParams)
    list_add(engineParams, delayParams)
    list_add(engineParams, wetParam)

    message(engineParams.SIZE)
    message(engineParams[1, 1]) // Equal to ENGINE_PAR_RV_DAMPING.

    activate_logger("C:/LogFile.nka")
    PrintAllEngineParams()
end on

function PrintAllEngineParams()
    declare i
    declare j
    for i := 0 to engineParams.SIZE - 1
        for j := 0 to engineParams.sizes[i] - 1
            print(engineParams[i, j])
        end for
    end for
end function

{
The data in the above section could be shortened by using list blocks.
    // Alternatively the above section could be written like this:  
    list engineParams2[,]
        filterParams
        reverbParams
        delayParams
        wetParam
    end list

    // Or even shorter, the values can be comma-separated on new lines.
    list engineParams3[,]
        ENGINE_PAR_CUTOFF, ENGINE_PAR_RESONANCE
        ENGINE_PAR_RV_SIZE, ENGINE_PAR_RV_DAMPING, ENGINE_PAR_RV_COLOUR
        ENGINE_PAR_DL_DAMPING, ENGINE_PAR_DL_TIME, ENGINE_PAR_DL_PAN, ENGINE_PAR_DL_FEEDBACK
        ENGINE_PAR_SEND_EFFECT_OUTPUT_GAIN
    end list
}

Functions for Setting UI Control Properties

These commands take an optional number of arguments so that the compiled code doesn't get unnecessarily bloated. There is a command for each UI control type, they have been chosen to set the most commonly used properties of each type. There is also a command called set_bounds(x, y, width, height) that works with any UI control type. Below is a list of all the commands and the arguments that each will take. The first argument is the control you want to use, it can either be the literal name of the UI variable or its UI ID.

set_bounds(control, x, y, width, height)
set_slider_properties(slider, default, picture, mouse_behaviour)
set_button_properties(button, text, picture, text_alignment, font_type, textpos_y)
set_knob_properties(knob, text, default)
set_label_properties(label, text, picture, text_alignment, font_type, textpos_y)
set_level_meter_properties(level_meter, bg_color, off_color, on_color, overload_color)
set_menu_properties(menu, picture, font_type, text_alignment, textpos_y)
set_switch_properties(switch, text, picture, text_alignment, font_type, textpos_y)
set_table_properties(table, bar_color, zero_line_color)
set_text_edit_properties(text_edit, text, picture, text_alignment, font_type, textpos_y)
set_value_edit_properties(value_edit, text, font_type, textpos_y, show_arrows)
set_waveform_properties(waveform, bar_color, zero_line_color, bg_color, bg_alpha, wave_color, wave_cursor_color, slicemarkers_color, wf_vis_mode)
set_wavetable2d_properties(wavetable, wt_zone, bg_color, bg_alpha, wave_color, wave_alpha, wave_end_color, wave_end_alpha)
set_wavetable3d_properties(wavetable, wt_zone, bg_color, bg_alpha, wavetable_color, wavetable_alpha, wavetable_end_color, wavetable_end_alpha, parallax_x, parallax_y)
declare pers ui_switch onSwitch
set_switch_properties(onSwitch, "", "")
set_bounds(onSwitch, 0, 0, 20, 20)

declare pers ui_slider volumeSliders[4](0, 1000000)
for i := 0 to num_elements(volumeSliders) - 1
    set_slider_properties(volumeSliders[i], 500000, "Knob")
    set_bounds(volumeSliders[i], 10 + i * 50, 10) // We do not need to set a width and height, so they can just be left out. 
end for

Iterate Macros

New command for iterating a macro in a similar way to a for loop. This is primarily useful in situations where Kontakt forces you to use a UI variable name instead of a UI ID number. First create a macro with one integer argument. Then use iterate_macro() command somewhere in your code to execute the macro a given number of times. The number of times is set in the same way a 'for' loop works. Note that if you set the range for iterating the macro in an invalid way (for example, minimum value is larger than maximum value), the macro simply won't be iterated at all!

define NUM_MENUS := 5

on init
    declare pers ui_menu menus[NUM_MENUS]
    macro addMenuItems(#n#)
        add_menu_item(menus#n#, "Item1", 0)
    end macro

    iterate_macro(addMenuItems) := 0 to NUM_MENUS - 1
end on 

macro macroCallbacks(#n#)
    on ui_control(menus#n#)
        message(menus#n#)
    end on
end macro

iterate_macro(macroCallbacks) := 0 to NUM_MENUS - 1

There is a also short way of iterating a one line macro. Simply put the line you wish to repeat in the brackets of the iterate_macro() command, and use token #n# to show the places where you want numbers to be substituted. At least one such token must be found to use this functionality.

define NUM_MENUS := 20
declare ui_menu menus[NUM_MENUS]
iterate_macro(add_menu_item(menus#n#, "Item", 0)) := 0 to NUM_MENUS - 1

Since version 1.12.0 there is now also iterate_post_macro(), which is a variant of iterate_macro() that is executed after the compiler expands all macros. This means that we can use macro arguments instead of define constants, allowing more flexible repeatable structures. For example:

iterate_post_macro(declare ui_button #n#) := #start# to #end#

Literate Macros

There is a related function to iterate_macro() called literate_macro() which iterates with a list of literal strings instead of with numbers. These literal strings are used by the function to call a macro multiple times, every time using a different string as the argument. In similar fashion to the iterate_macro() function, you first need to create a macro with one argument, but this time the argument is text replacement. The function expects a list of comma-separated strings - it is often useful to define this beforehand. This function also has a shorthand for one line macros, this time using #l# as the token instead of #n#. However, #n# token can still be used in one line literate macros, to use the ordinal number of the literal as an argument.

define SLIDER_NAMES := volume, pan, tune

on init
    literate_macro(declare ui_slider #l#_slider (0, 100)) on SLIDER_NAMES
    literate_macro(message(#l#)) on "s1", "s2", "s3"
end on

macro sliderCallbacks(#name#)
    on ui_control(#name#_slider)
        message(#name#_slider)
    end on
end macro

literate_macro(sliderCallbacks) on SLIDER_NAMES

macro sliderCallbacks2(#arg1#, #arg2#)
    on ui_control(#arg1#_slider)
        message("Slider #arg1# has ID #arg2#")
    end on
end macro

literate_macro(sliderCallbacks2(#l#, #n#)) on SLIDER_NAMES

Since version 1.12.0 there is now also literate_post_macro(), which is a variant of literate_macro() that is executed after the compiler expands all macros. This means that we can use macro arguments instead of define constants, allowing more flexible repeatable structures. For example:

literate_post_macro(declare #l#) on #obj#.CONTROLS

Since version 1.18.0 it is now possible to append or prepend literal entries to an existing define macro by using operators += for appending, and =+ for prepending. For example:

define FOO := age
define FOO += height
define FOO += weight

define FOO =+ name, surname

on init
    literate_macro(declare #l#) on FOO
    literate_macro(message(#l#)) on FOO
end on

This will produce the following output:

on init
    declare $name
    declare $surname
    declare $age
    declare $height
    declare $weight
    message($name)
    message($surname)
    message($age)
    message($height)
    message($weight)
end on

Automatic Number Incrementer

There is now a built-in text replacement macro for incrementing a number over lines of code. Each new line the macro will be incremented by an amount specified. The syntax to start the incrementer is START_INC(text, startNum, stepNum). You can then end the incrementer with END_INC. The incrementer is substituted after normal macros are made, this makes it slightly more powerful than just a list of numbers in the source.

on init
    START_INC(N, 0, 1)
    message(N)     // message(0)
    message(N & N) // message(1 & 1)
    END_INC

    f()
    declare list !menuItems[]

    START_INC(N, 0, 1)
    CreateMenuItem(LFO, "LFO")
    CreateMenuItem(ENV, "Envelope")
    CreateMenuItem(CC , "CC Num")
    END_INC
end on

function f()
    START_INC(N, -2, -1)
    message(N) // message(-2)
    message(N) // message(-3)
    END_INC
end function

macro CreateMenuItem(ID, text)
    declare const ID := N
    list_add(menuItems, text)
end macro
⚠️ **GitHub.com Fallback** ⚠️