Yazoo ---> Online Help Docs ---> Yazoo scripting ---> Functions

Search Paths and Fibrils

We can imagine Yazoo's memory as a graph in the mathematical sense: a swarm of dots representing the variables, connected by arrowed lines which represent the members. In this picture a ---> b means that some member of a points to b. When we surf Yazoo's memory using the `.' and `[]' operators, we are exploring this graph in the `forward' direction; i.e. along the direction of the arrows. We never explicitly navigate backwards, since members are one-way channels; however, Yazoo does swim back upstream on its own when the user requests, say, a global variable from within a function. In doing so it follows a so-called `search path'. The totality of search-paths forms a separate, parallel web of arrows atop our memory map.

Let's first look at how a search path works in general terms. Suppose that we look for


    recipes[13].ingredients[3, 5].amounts
   

The recipes array may be in the active function, but it might also be in some enclosing package or in the global space. Yazoo checks the first possibility first: it looks for recipes in the active function. If it does not find any recipes here, then it goes up to the enclosing object and searches there. If recipes is not there either then Yazoo backs up one more layer; etc. on backwards until it either finds what it is looking for or reaches the end of the road.

After locating the starting point of the path (recipes) in reverse, Yazoo shifts into forward gear for the rest of the expression. Each successive step [13], ingredients, [3, 5] and amounts must refer to a direct member of the previous step. (The exception is when the expression comes left of a define statement: if recipes, or any subsequent step, cannot be found, that and everything that follows will be created on the fly.)

This section will be concerned with an unresolved issue regarding the first, reverse step. The problem is that, due to aliasing, the current function might be targeted (`enclosed') by any number of members from different locations. So which one does it step back into, in reverse? Yazoo only takes one unique backwards path. The answer is that if Yazoo has to search backwards, then it backs up into whichever object defined the function and looks there -- even if the member leading to the function was removed in the meantime. Then it tries the object that defined the object that defined the function, and so on.

As an example, suppose the following object was defined in some part of a script:


    picture :: {
       bitmap[10][10] :: ubyte    | data
       vertices[4] :: ulong
       
       draw :: { ..., code, ... }    | methods
       crop :: { ..., code, ... }
    }
    picture.reset :: { ..., code, ... }  | externally defined method
    picture.crop :: picture.crop : { ..., code, ... }    | specializes the old crop()
   

Now suppose that, at a different part of the script and from some very distant location in memory, the following alias has been made:


    shortcut = @picture.draw
   

For all purposes but one, the two members draw and shortcut are completely equivalent. The only distinction between the two members, and the only justification for referring to that function by the name `draw', is that its search path will pass through the picture class where it was born, rather than through the object containing shortcut.

In general, the first step in a pathname can only find members that were defined immediately inside objects that, at some level, also defined that code. All other members have to be accessed by subsequent steps. In the example above, draw() is permitted access to all the members of the picture class because in some sense the definition of draw() is part of the definition of picture. But draw()'s members and crop()'s members lie on different and disjoint branches of code, so they must explicitly write draw.whatever, crop.whatever to access each others' members.

Finally, the reset() function and the second half of crop()'s code were not defined by picture, so their search paths skip directly from their respective variables to whatever object defined picture. Thus, whereas draw() has permission to call up bitmap[3] directly, its adopted sibling reset() must explicitly request picture.bitmap[3]. The same of course holds true for foreign code that isn't even tied to a member of picture, such as the following.


    (picture.draw << { code, print(bitmap)})()  | won't work
   

It's important to keep in mind that whenever an instance of the picture class is defined, instances of draw() and crop() will be defined and associated with the new variable, but crop() will only have the first half of its code and reset() will not even exist. The reason is that the code within picture's braces, which can be thought of as picture's constructor, makes no mention of these additional components.

Just as forward steps in memory are taken one member at a time, steps in a reverse search path go one `fibril' at a time. A fibril is a little bit like a member in reverse: its stem (the thing it points to), is the next step in the search path, which is usually (but not always) the previous step from some forward path. Suppose we have


    a.b :: { code, print(a) }
   

A member goes from `a' to `b', and a contrary fibril goes from `b' back to `a'. (The fibril is what allows `b()' to find and print `a'.) Each fibril is associated with the variable that is the given step on the search path.

Members and fibrils have significant differences beyond the directions of their arrows. Obviously, only members have names. Less trivially, whichever variable a member connects to may have many members of its own, so there can be many different forward paths from a given starting point. Each fibril, on the other hand, connects directly to another fibril, not that fibril's variable, so there is a unique fibril-to-fibril path backwards. One final difference is that, in the forward direction, we can go in loops if members target their own variables or parents (think a.b.b.b if a :: { b := @this }). In doing so we encounter the same member many times. But while we are going in circles, Yazoo keeps unrolling an ever-growing fibril trail that traces our circuitous path like a ball of yarn --- and if we define a code at the end of all that, then that code will take the same redundant journey backwards in searching for variables (search `a' four times before making it to the enclosing variable). The same fibril is never encountered twice on a given path (which is a good thing, since otherwise Yazoo might go in circles forever); and the implication is that the fibril network forms a true tree, or set of trees -- no loops.

Every piece of code in Yazoo has a fibril `anchor', where the search for members begins while that code is running. The anchor is usually (except in the case of substitution) attached to that code's own variable, because a function looking for a member always looks inside its own belly first. If a function has two codes spliced together with the inheritance operator (à la our crop() function above), then each code will have a separate anchor and thus potentially different search paths. Whenever Yazoo needs to find a member, it simply grabs the anchor of the code that is currently running and follows backwards until it either finds what it is looking for, or else hits the end and throws an error.

Fibril trees are built up one branch at a time. A new branch (fibril) is added each time a pair of braces is encountered. The rule is that any new function or composite object obtains, firstly, a variable of its own; and secondly, an anchor in that variable whose stem is the anchor of the code that defined the new object.

Search paths may be invisible to the user, but that doesn't mean we can't operate on them. Yazoo comes equipped with two tools -- the code-substitution operator `<<' and the built-in function clip() -- which together can perform fairly arbitrary surgery on the fibrils. Code substitution temporarily grafts one fibril onto another fibril's stem. The clip() function permanently and irrevocably terminates a fibril path at a given point.

Code substitution is usually embedded within a function call, as in (var << fn)(). Earlier we described the object var << fn as a union of the code of fn with the variable var; now we can give a more sophisticated and formal explanation. The code substitution operator replaces the anchors of fn's codes---temporarily---with new anchors inside of var. However, these new, temporary anchors in var inherit the original stems that fn's normal anchors have. That means that the first stop on the search path is var, but the next stop is the parent of f2, followed by the grandparent, etc.

Code substitution is a temporary drug in the sense that code doesn't stay substituted outside of the immediate expression. Even other instances of var and fn in the same sentence won't be affected. One permanent side effect, however, is that any functions that were defined during the substitution will be stuck with the crooked, `temporary' spliced fibril path for the rest of their lives. So suppose that fn had defined some new function daughter when it ran inside of var. Then daughter would have a search path leading from itself back to var, and thence back to fn's parent.

It is the balance between the need to secure data and Yazoo's desire to keep all its code `open-source' that justifies the logic behind all this. On the one hand, any code is free to access any variable that it can draw a path to, so strict encapsulation of data is practically impossible (see next section). Requiring explicit paths to members of functions other than one's own is what is supposed to prevent accidental intrusions into one's neighbors. Everybody's door is unlocked, but you have to make a deliberate effort to burgle someone else's house. But, a function can access its parent's and grandparent's members without any effort, because that function is also under their roofs: its definition was part of its parent's and grandparent's codes. None of these rules change upon code substitution. A function can still draw on the resources of its biological parent when it is substituted. But not the parent of its temporary workplace -- being hired to paint the child's room doesn't give anyone permission to whitewash the apartment.

Clipping a fibril is a little bit like disowning one's child. The clip() command simply unhooks a fibril from its stem, isolating any code whose search path used that fibril from everything beyond the clip point. For example, if we write


    a :: double
    b :: { ; print(a) }
   

then running b() works until we clip(b), at which point b() can no longer see `a'. Of course it would still be able to see its own members, if it had any, since they would be above the clip point.

In contrast to the code-substitution operator, the effect of a clip() is both permanent and irrevocable. It might seem unbalanced that clipped functions can't reach their mother functions, while the mothers still have full access to their daughters, as clip() only affects fibrils, not members. But on this point the law is perfectly even-handed. Daughters can disown their mothers without leaving house by using the remove command, which deletes the member while leaving the corresponding fibril intact.


Prev: Code substitution   Next: Classes and Inheritance


Last update: July 28, 2013

Get Yazoo scripting language at SourceForge.net. Fast, secure and Free Open Source software downloads