When Cicada is run interactively, start.cicada runs the script file user.cicada file which simply predefines a number of useful constants and functions in the workspace. As its name suggests, the user is absolutely encouraged to customize user.cicada in whatever way makes it most useful.
Constants and variables
passed:
0, denoting the error code of a function that did not cause any error.
e:
the exponential constant. Rather than provide an exp() function, Cicada defines e so that the user can evaluate exponentials by writing e^x, e^-2, etc.
pi:
the famous constant pi.
inf:
infinity.
nan:
not-a-number (used by floating-point arithmetic).
root:
an alias to the user’s workspace.
where:
the current search path of the user, stored as a string.
filePaths[]:
a string array of pathnames to folders. The user.cicada routines (but not the built-in Cicada functions!) will search each of these paths when looking for a file. user.cicada preloads the empty path, which is usually the Cicada directory. We can change the search paths just by manipulating this set: e.g. filePaths[+2] = "/Desktop/".
We will skip the custom compiler definition here since they are explained in Chapter 4. Next is a function that fixes one of the more awkward aspects of Cicada’s use of functions.
Function calling
new()
syntax: (same type) var2 = @new((any type) var1 [, (variable) data_to_copy] [ ; modifying code])
The new() function returns a new instance of a variable, of the same type as the old and storing the same data. This is particularly useful within functions, where, as emphasized earlier, it is usually wise to create a new return variable with each function call. Instead of writing
f1 :: {
...
((rtrn =@ *) @:: double) = x + y
return rtrn }
we accomplish this with
f1 :: {
...
return new(x + y) }
new() will not work if the variable’s type does not match its current contents, which can happen if the variable had been modified after being created:
comp1 :: { a :: int }
comp1.b :: char
new(comp1) | cannot copy the data
In this case new() will create a new variable using comp1’s constructor, but it will not be able to copy the data over and will output a warning. There are two ways to fix this. 1) The optional second argument to new() decouples the variable that provides the type specification (first argument) from the variable that provides the data (second argument). If this second argument is void (as in new(v, *)) then no data is copied. 2) Any coding section in the arguments are run inside the new variable immediately after it is created, allowing us to modify the variable before the data is copied.
Note that new({a, b}) doesn’t make new instances of a and b, but rather just returns a new set of aliases to those variables.
File input and output
Load()
syntax: (string) filedata = Load((string) filename)
Load() (capital ‘L’) extends the built-in load() function by searching all paths in the DirectoryNames[] array.
Save()
syntax: Save((string) filename, (string) filedata)
Save() (capital ‘S’) extends the built-in save() function by searching all paths in the DirectoryNames[] array. This is important when a filename such as archive/mail.txt is provided, since the archive/ folder may not be in the default (./) directory.
cd()
syntax: cd((string) filepath)
The easiest way to change Cicada’s file-search directory. cd() resizes the filePaths[] array to size 1 and sets that to the string given as its argument.
pwd()
syntax: pwd()
Prints all file directories (all entries in the filePaths[] array) to the screen.
String operations
lowercase()
syntax: (string) lowercase_string = lowercase((string) my_string)
Converts a mixed-case string to lowercase.
uppercase()
syntax: (string) uppercase_string = uppercase((string) my_string)
Converts a mixed-case string to uppercase.
C_string()
syntax: (string) string bytes = C_string((string) my_string)
Cicada strings are normally stored internally as linked lists. C_string() converts a length-N resizable Cicada string to a N+1-byte C-style string containing a terminating 0 character.
cat()
syntax: (string) concatenated string = cat((variables) var1, var2, ...)
Returns a string which is the concatenation of the arguments. This is just a convenient implementation of the print_string() function: s = cat(v1, v2) is equivalent to print_string(s, v1, v2).
Printing routines
printl()
syntax: printl([data to print])
This function is the same as print() except that it adds an end-of-line character at the end.
sprint()
syntax: sprint([data to print])
sprint() is used for printing composite objects such as variables and functions; the ‘s’ probably originally stood for ‘spaced’, ‘set’, or ‘structure’. This is one of the most useful functions. It prints each member of an object separated by commas, and each composite object is enclosed in braces. Void members are represented by asterisks. The output is in exactly the format that Cicada uses for constructing sets.
> sprint({ a := 5, b :: { 4, 10, "Hi" }, nothing }, 'q')
{ 5, { 4, 10, Hi }, * }, q
sprint() is the default calculator (i.e. calculator aliases sprint()).
mprint()
syntax: mprint([data to print] [ ; (ints) fieldWidth, maxDigits, (string) voidString = values ])
This ‘matrix’ print function prints tables of numbers. Each index of the argument is printed on a separate line; each index of a row prints separately with a number of spaces in between. For example:
> mprint({ 2, { 3, nothing, 5 }, { 5/2, "Hello" } })
2
3 * 5
2.5 Hello
mprint() has three user-adjustable optional parameters that can be changed in the argument coding section. mprint.fieldWidth controls the number of spaces in each row; it defaults to 12. mprint.maxDigits controls the precision of numbers that are printed out; it defaults to 6. A maxDigits of zero means ‘no limit’. mprint.voidString is the string used to represent void members.
Reading/writing tables and data structures
writeTable()
syntax: (string) table_string = writeTable((table) data [ ; (ints) fieldWidth, maxDigits, (string) voidString = values ])
writeTable() exports table data as a string. This function takes the same three optional arguments as mprint().
saveTable()
syntax: saveTable((string) filename, (table) data [ ; (ints) fieldWidth, maxDigits, (string) voidString = values ])
The saveTable() routine exports data stored a set or array to a file. This routine attempts all file paths when saving, just like user.cicada’s general-purpose Save() function. The optional arguments are the same as those used by the function mprint().
readTable()
syntax: readTable((table) table_array, (string) table_text [ ; (bools) ifHeader, resizeColumns, resizeRows = values])
The counterpart to saveTable() is readTable(), which loads data into an array. It reads the data from a string, not a file, and tries to parse the data into the provided table. If the IfHeader variable is set to true, then the first line of text is skipped. Setting the Resize...Index arguments gives readTable() permission to adjust the size of the table to fit the data; in order for this to work the table must be a square array (i.e. not a list of 1-dimensional arrays that can be resized independently). The default values of the optional arguments are false for IfHeader, and true for ResizeFirstIndex and ResizeSecondIndex. An error results in a non-zero value for readTable.errCode and an error message printed to the screen.
readInput()
syntax: readInput((table) table_array [ ; (bools) ifHeader, resizeColumns, resizeRows = values])
Identical to readTable(), except reads the table string from the command line input.
readFile()
syntax: readFile((table) table_array, (string) file_name [ ; (bools) ifHeader, resizeColumns, resizeRows = values])
Identical to readTable(), except reads the table string from a file. Searches all directories in the filePaths[] array.
Running code
run()
syntax: (numeric) script_return_value = run((string) filename [, (composite) target])
The essential run() function runs a script stored in a file. run() compiles, transforms and finally runs the code in the current go{} location and search path. Any errors in the process are flagged along with the offending text. run() searches all directories in the filePaths[] array. If there is a direct return from the lowest level of a script (i.e. not within a function or type definition) then the return variable will be handed back to the calling script.
Normally the specified script is run in the user’s workspace. Optionally, we can pass some other variable or function as a second argument to run(), in which case the script runs inside that object instead.
A given script is often run multiple times. By default, when executing a script run() first checks to see whether it has seen that script before, and if so removes any root-level objects that the script defined when it was last run. This is to avoid type-mismatch errors when the script tries redefining those objects. If this is a problem then set run.CleanUp = false. (This parameter is not set within the arguments.) To make sure it knows when a script was rerun, make sure that the Boolean run.caseSensitive is set properly for your file system (it defaults to false meaning that Cicada assumes the file system doesn’t discriminate filename cases).
do_in()
syntax: do_in((composite) target [, search path [, code_args [, bytecode_mod_args]]] , code, base script [, code, code modifying bytecodeWords[]])
The do_in() tool allows one to run code in a specified location and with a specified search path, and gives the option of manually modifying the bytecode before it is run. The idea is that it is easier to write bytecode by perturbing a compiled script than to write everything from scratch.
The first argument to do_in() is the variable to run the code inside. The optional second argument gives a customizable search path, and it exactly mirrors the optional third argument to transform() (see the reference on transform() for how to specify a path). The third and fourth arguments, if given, are passed as args[1] for the script to be run and the bytecode-modifying script respectively.
Following the first code marker we give the text of the script that we want to run, or the closest that the Cicada compiler can achieve. Often this is all we need. On occasion we may wish to modify the compiled bytecode of the baseline script before it executes, perhaps to achieve something that is unscriptable. do_in() accommodates this need by running, in unusual fashion, the code following an optional second code marker/semicolon in its argument list (if that exists) after compilation but before execution. At that time the compiled baseline script will be stored in an array entitled bytecodeWords of integers, and we may alter in any way whatsoever provided the bytecode comes out legitimate. In the extreme case we can give no baseline script and simply alias bytecodeWords[] to an existing integer array that is already filled with bytecode.
Here we show how to use do_in() to create an unjammable alias to some variable var1, which cannot be done using ordinary Cicada scripting.
do_in(
root
code
al := @var1
code
bytecodeWords[2] = that + 128 | add an unjammable flag
)
compile_and_do_in()
syntax: compile_and_do_in((composite) target [, search path [, code_args [, bytecode_mod_args]]] , code, (string) base script string [, code, code modifying bytecode[]])
Compiles a script, optionally modifies it, and then executes the script in the provided directory. This is equivalent to do_in() except that the script is stored as an uncompiled string rather than compiled code. We write the arguments just as we did for do_in(), except with an extra pair of double-quotes around the code to compile (even though it’s in the coding section of the arguments). The analog of the do_in() example would be:
compile_and_do_in(root; "al := @var1"; bytecodeWords[2] = that + 128)
Working variable
go()
syntax: go([ code, ] path)
Cicada’s go() function changes the working variable for commands entered from the prompt. A search path is dragged along behind that leads eventually back to root (the original workspace). To see how this works, type:
> a :: { b := 2 }
> go(a)
> b | we are now in 'a', so this is legal
2
> a | search path extends back to root, so we can see 'a' as a member
{ 2 }
The search path exactly backtracks the given path. If one types go(a[b].c().d, then the working variable is ‘d’, and the search path goes backwards through (in order): the return variable of ‘c’, then ‘c’ itself, then the b’th element of ‘a’, then ‘a’ itself and finally root. Typing just go() sends one back to the root; typing go(root) is actually not quite as good because it puts root on the path list twice. To see the path, look at the global pwd variable.
go() works by updating the go_paths[] array defined by start.cicada. Each command entered from the prompt is transformed and run according to the current state of go_paths, so invoking go() does not take effect until the next entry from the prompt. Thus it was necessary in our example to separate the second and third lines: go(a), sprint(b) would have thrown a member-not-found error. For the same reason, while running a script (via run()), go() will do nothing until the script finishes -- use do_in() instead.
When the user calls go(...), Cicada constructs the argument list before go() itself has a chance to run. Owing to this fact, certain sorts of go-paths will cause an error that go() can do nothing about. For example, go(this[3]) will never work because ‘this’ is construed as the argument variable, not the working variable. To get around this problem, go() gives us the option of writing the path after a code marker or semicolon, as in go(code, this[3]), as those paths are not automatically evaluated. A code marker is also useful if we need to step to a function’s return variable but don’t want the function to run more than once. go(code, a.f().x) will evaluate f() just a single time in the course of go-processing, whereas for technical reasons f() would have run twice had we not included the code marker.
go() at present has many limitations. Each path must begin with a member name or this, and all subsequent steps must consist of step-to-member (a.b) and step-to-index (a[b] and related) operations and function calls (a()). No [+..] or +[..] operators are allowed. The step-to-index operations are particularly dicey because of two nearly contradictory requirements: the path can only step through single indices, and for practical use the path must nearly always span complete members (i.e. all of the indices of an arrays). Although the latter is not a hard requirement, it is really hard to do anything meaningful within a single element of an array, because so many common operations involve creating tokens and hidden variables which can only be done for all elements of the array simultaneously. Even trying to reset the path by typing go() will not work at that point, so in this sticky situation start.cicada will eventually take pity and bail the user out. The upshot of all this is that go() does not work very well inside of arrays.
jump() is a similar operation to go(), except that go() can shorten a path whereas successive jumps keep appending to the current search path.
jump()
syntax: jump([ code, ] path)
jump() is basically identical to go() except in the way that it handles the first step in a search path. For most details, see the explanation of go() above. The difference between the two functions can be seen by example.
> a :: { b :: { ... } }
> go(a.b), where
root.a.b
> go(a), where | starting from a.b
root.a
> go(b), where
root.a.b
> jump(a), where | again, starting from a.b
root.a.b-->a
jump() takes advantage of the fact that search paths in Cicada can twine arbitrarily through memory space; we don’t have to restrict ourselves to paths where each variable is ‘contained in’ the last. A more useful path would be something like root.a.b-->c.d: that would allow us to work inside of ‘d’ while retaining access to ‘a’ and ‘b’, even if those latter lie along a different branch.
what()
syntax: (string) var_names = what([ (composite) var_to_look_in ])
Returns the names of the variables in the current directory, which is usually root (see go() and jump()). If an argument is provided then what() returns the names of the variables inside that argument variable. Remember that what() requires the parentheses!
Numeric
min()
syntax: (numeric) result = min((numeric list) the_list [, code, rtrn = { index / value / both])
Returns the minimum element of a list: its index, value (the default), or the combination { index, value}.
max()
syntax: (numeric) result = max((numeric list) the_list [, code, rtrn = { index / value / both])
Returns the maximum element of a list: its index, value (the default), or both { index, value }.
sum()
syntax: (numeric) result = sum((numeric list) the_list)
Returns the sum of elements of a numeric list.
mean()
syntax: (numeric) result = mean((numeric list) the_list)
Returns the average (arithmetic mean) of the elements of a numeric list.
round()
syntax: (numeric) rounded_integer = round((numeric) real_number)
Rounds a real number to the nearest integer. For example, 1.499 rounds to 1, 1.5 rounds up to 2, and -1.5 rounds ‘up’ to -1.
sort()
syntax: sort((table) table_to_sort, { (list) sort_by_list or (numeric) sorting_index } [, code, direction = { increasing / decreasing])
Sorts a list or table, which is passed as the first argument. If it is a table then a second argument is required: either the column number to sort by, or a separate list to sort against. So the following two sorts are equivalent:
myTable :: [10] { a :: b :: double }
for (c1::int) in <1, 10> myTable[c1] = { random(), random() }
sort(myTable, 1) | sort by first column
sort(myTable, myTable[*].a)
The sort-by list will be unaffected.
Whether to sort in increasing or decreasing order can be specified after the semicolon/code marker; the default is ‘increasing’. The column to sort by, whether it is in the same table or in a separate list, must be numeric; sort() will not alphabetize strings (although it will work with character fields).
binsearch()
syntax: binsearch((table) table_to_search, (numeric) value_to_find)
Searches a sorted list for a given value. The list must be numeric (char-typed lists are OK). If the list is not sorted then binsearch() will probably not find the element.
Bytecode
disassemble()
syntax: [(string) disassembly = ] disassemble((string) compiled_code [ , (string array) name_space [ , (int) start_position ] ] [ , code, (bool) expandFunctions, (int) flagPosition = values ])
The disassemble() function returns a textual interpretation of compiled Cicada bytecode. The first argument is a string containing the bytecode. The optional second argument allows the user to pass a different namespace (a string array) other than allNames[], or * to avoid printing member names. The function will return the ‘disassembly’ as a readable string. Used by the author to satisfy the odd craving for a rush of bytecode:
> disassemble(compile("x = that + 2", *, *, allNames))
By passing a third argument, the disassembler can be used to skip over a bytecode expression. In this case the disassembler will only disassemble up to the end of the expression, and if the starting word index was passed in a variable then that variable will be updated to the beginning of the next expression. For example, we can use this feature to write a function that finds the Nth command in a compiled expression.
go_to_Nth_sentence :: {
code
code_string := args[1]
N := args[2]
code_index := 1
for (n :: int) in <1, N-1> &
disassemble( code_string, *, code_index )
return new(code_index)
}
When run in this ‘skip’ mode, disassemble() does not return any bytecode string. If you want the output string you should first find the end of the expression that start_position begins, then do a full disassembly on just that expression.
The expandFunctions option determines whether inlined code definitions (as in, objects defined within curly braces) are disassembled (true is the default), or skipped with an ellipsis if false. If flagPosition is set to an integer value then the disassembler will flag that bytecode word, which is useful for marking errors.
Last update: May 8, 2024