There’s something missing in our scripts, and the easiest place to see this is in an ordinary function call:
f(2, pi, { 1, int })
Realizing that commas are just ends-of-lines, we could write this
f(
2
pi
{ 1, int }
)
which shows that 2, pi, and { 1, int } are somehow all valid commands. How can this be?
Let’s draw an analogy. Back in the cave man days, there were probably a lot of sentences like
rock
which in modern English would be
[That which I want to draw your attention to] [is] the rock.
In other words, if a sentence only contains an object, the cave man’s brain fills it out by adding some stock subject and verb. Cicada works exactly the same way: the stock subjects and verbs to use in different situations are the so-called adapters defined in cicada.c. Each adapter allows the compiler to convert some bare expression (e.g. the object of a sentence) to another type (a full sentence) by throwing in a few extra bytecode words.
When the user enters the command ‘2’, the compiler rolls its eyes and reaches for the type-mismatch error button, because it a complete command is a type 1 expression whereas an int_constant can only be construed as types 4, 5 or 6 based on its return-types string in the cicadaLanguage[] array. But then Cicada notices an adapter that works on type-5 objects (named type5arg_adapter in cicada.c), and moreover that adapter’s return-types string includes a “1” which is what we want. So the adapter adds its code and the error never happens. Here is the adapter’s bytecode:
inbytecode "8 173 10" anonymousmember "a1"
The expression to adapt is considered the argument and goes in place of a1. Looking at cicada.h or the reference section, we could figure out that this adapter turns our expression into something looking like
var1 := 2
except that the define-equate operator has slightly different define flags (173 instead of 47).
Other types of objects use different adapters when they appear by themselves. Variables use an adapter that creates an alias: for example the expression ‘pi’ becomes something like
var2 := @pi
though again using slightly different flags from a normal aliasing operator. Finally, type-objects are assigned turned into full commands using a third adapter that adds a define-like operator. Thus ‘{ 1, int }’ turns into a modified version of
var3 :: { 1, int }
or more precisely:
var3 :: { var3a := 1, var3b :: int }
The anonymousmember keyword produces a unique member ‘name’ that is inexpressible by the user. (Names become ID numbers in the bytecode: user-typed names become positive ID numbers, whereas anonymous members get assigned sequential negative ID numbers as they are encountered. The namespace consists of both the name-ID list and the negative ID counter.) Thus var1, var2, etc. in the last paragraph don’t really don’t have those names or any other, so function f() will access those members using the bracket operators (args[1], args[2], etc.). It’s technically possible to write bytecode having hardcoded negative IDs to access anonymous members, but that’s a last resort usually used for anonymous members that are also, in the lingo, hidden.
Hidden members are invisible to the array-index operators. In bytecode-speak, these are produced using define operators whose hidden-member flags are set (flag 6 in Table 3). The compiler sprinkles anonymous hidden members discreetly around the code for a variety of reasons, where most of them operate almost undetectably. The one exception: a hidden-define operator embedded in each function call creates a hidden member in the calling space to store the arguments, which becomes directly visible within the function itself through the pseudo-member args. (Maybe a better notation would show this member explicitly using braces for function calls, as in f{x, y}). The rarest bird of all is the hidden-define-minus-constructor operator (def-c** in the table), living exclusively in trap() function calls, where it defines the args variable without running its constructor so that trap() can do so in a controlled way.
The other define-operator flag used by (in this case all) adapters is the unjammable flag (flag 7; see Table 3), which prevents members from jamming arrays. Consider the function call f(myArray[<2, 4>]), which produces a hidden args variable consisting of { myArray[<2, 4>] }, which compiles to something like { anon1 := @myArray[<2, 4>] }. Ordinarily anon1[] would jam myArray[], and the actual array could not be resized from either member since doing so would also force a resize of the other. But in this case anon1 was defined as unjammable, as in unjam-able (can be unjammed), so any array resize (e.g. myArray[+3]) is allowed because the now-out-of-date anon1 gets de-aliased rather than causing a jammed-member error. That’s OK because the alias will be restored the next time that same function call happens, since the argument constructor will be rerun. (However there can be a problem in other contexts where the constructor is not rerun each time it is used, for example in sets containing aliases to array subsets. Use hard-coded aliases for these cases.) As an aside, these adapters also clear their update-member-type flags (flag 1), so that their anonymous members can be re-assigned to variables of different type (in case, for example, between two iterations of the command f(a) member ‘a’ gets removed and redefined).
One last type of adapter called noarg_adapter replaces missing expressions altogether. These adapters are necessary to allow blank scripts, or situations like two consecutive end-of-lines (command-conjoining operators) which lack a command between them to conjoin. Another no-argument adapter allows a script to contain a return command without a variable. The final set of no-argument adapters in cicada.c is used to convert a sentences-type expression (type 1) to a script expression (type 0), by adding a null bytecode word at the end.
Last update: May 8, 2024