Inq Hierarchical Data Structures

The Containment Hierarchy

Inq uses the recursively defined data structure of a node that can contain any number of named child nodes to represent all aspects of an application. All entities, whether data, algorithms, user interface components, indeed anything accessible from the Inq language are represented by such nodes. The exact type of a node, and therefore the operations that can be performed on it, is not known until runtime.

Nodes that can contain named children are Maps, that is, such a node yields a chosen child by the application of some key.

Node Paths

To address a particular node a node path is applied from some starting point. The simplest type of node path takes the form

foo.bar

A path consists of a number of elements, each separated by a delimiter. In the above example the elements are Inq identifiers and the delimiter is the period character.

Node path

When applied to the structure shown in the diagram the result is the string "Hello, World".

If a path does not resolve then its result is unresolved null (discussed further when considering the null constant).

The Context Node

An Inq process environment provides a root node under which all aspects of the application running within that process are built. Inq script runs against a specific node, known as the context node. This is analogous to the concept of a 'current working directory'.

Node path

The context node is the node from which all path specifications qualified with the $this prefix will be applied. In the above diagram, for example, if the context node were specified by the path p.q.x then the context node would be node4. A context node typically defines the root of a particular sub-division of an application. Two different contexts may hold similar items beneath them, if they represent two instances of, say, a Customer Editor tool.

Because the context node is a descendent of a process's root node, any structure built there remains accessible for the lifetime of the process or until wilfully removed.

The Stack

When Inq script runs a call stack is established. The stack is used to pass arguments and hold temporary variables. Subsequent stack frames are created as functions are called.

The stack can be referenced with paths of the form $stack.p.q but because the stack is referenced most often it is the default starting point for path resolution and the $stack. prefix is optional.

Stack frames are unwound as functions return to their caller. Anything built on the stack becomes unreferenced once its frame is unwound.

Path Prefices

If a path contains no special prefix then it is applied relative to the stack. However, there are a number of prefix tokens that have the following meanings

Token Meaning
$this Resolves to the current context node.
$root Resolves to the process's root node.
$stack Resolves to the current stack frame. This is the default.
$process Resolves to the executing process.
$loop Resolves to the iteration loop child.
$path Resolves to a path which is the path specification to the node given by $this.
$catalog Resolves to the global catalog available to all processes.
$properties Provides access to the JavaTM system properties
$uidefaults Provides access to the User Interface default property settings.

Identifiers and Reserved Words

Inq has a significant number of reserved words arising from its syntax and built-in functions. A path containing more than one element is always recognised as such because the presence of delimiters remove any ambiguity, however single-element, stack-assumed paths will generate an error if they use a reserved word. This can be resolved by the (benign) prefix of the period character. For example, Inq supports a length function to return the length of a string. The following paths are legal:

vars.length;
.length;

However this is not:

length;

Complex Path Formats

Inq supports a syntax in node paths for substitution of path elements and node access by vector.

Path Element Substitution

A simple path specification is appropriate where a node path is known in advance and does not require run-time substitution. Under some circumstances, however, it is desirable to evaluate one or more components of a path specification as the result of other path references. Paths of this nature take the form

a.{p.q.r}.c;

A path element contained within braces is itself evaluated and the result used as the key applied at the current step in path resolution. In the example, the result of the path p.q.r is applied to the node at a to yield the appropriate child node. This node, in turn, then has the path c applied to it.

If the path p.q.r resolves to a path() (see below) then its elements are used to continue the resolution. As a convenience, if a string is yielded then it is tokenized into individual path elements. In the above example, if p.q.r resolves to x.y.z then the effective node specification overall is a.x.y.z.c

The substitution does not have to result in a path or a string and a result of any other type is applied as a single path component. An example of the use of this type of substitution is given when discussing building node structures.

Vector Access

If a map is orderable then its children can be accessed by a zero-based vector index as well as by name. The syntax for such a path is as follows

customers[3].ContactDetails.Fax;

The square bracket can contain either

  1. the literal string @first
  2. the literal string @last
  3. a literal integer, (0 is not equivalent to @first, see below)
  4. a node path that resolves to an integer

An index that is out of range generates an exception. This includes the value zero when the vector is empty, however sometimes it is desirable to allow this case to result in unresolved null. Applying a path of the form customers[@first] results in the zero-th vector entry or unresolved null when the vector is empty:

any firstEntry = customers[@first];
if (firstEntry)
{
  // Do something with firstEntry
}

Using @last is a more succinct and efficient expression of

any entries = count(customers) - 1;
customers[entries]

As well as ordered maps, Inq also supports arrays which offer vector access only. The capabilities of the various container nodes available are discussed in the section on Container Types. A run-time error occurs if the node at which the vector access is applied does not support vector addressing.

Lazy Evaluation

Path elements may be delimited with the * character, which indicates lazy evaluation of the node path. In this case, if the current node does not contain the following element a breadth-wise iteration is performed to search for it. Referring to the above diagram, if a path of p.q*s is applied starting from node1 it is undefined whether node7 or node9 would be returned.

Lazy evaluation can be used to address nodes, however it is more often applied when specifying paths as a way of discriminating events. This is covered when discussing how Inq scripts can explicitly listen for events.

The path Function

Certain Inq functions require a path argument, rather than the node it resolves to. The path function protects a path from being resolved:

path(p.q.r);

If the argument contains substitutions then path attempts to resolve them. Those that succeed are placed in the resulting path, however any that fail remain unresolved in the path until it is applied somehow. Vector access is always resolved every time a path is applied. Here are some examples in pathtests.inq that illustrate these outcomes. Output is interspersed where appropriate:

// Set up some node structure to test our paths against.
// First, create an ordered map (so we can show vector access as well)
omap g.h;

// Add some nodes. Use numbers that make vector tests obvious
// (ok vectors start from zero but you know...)
int g.h.a1.n = 11;
int g.h.a2.n = 22;
int g.h.a3.n = 33;

// Value to test vector access with. Should result in node a3
int m = 2;

// Declare a vector path.
any vp = path(g.h.[m].n);
$stack.g.h.[$stack.m].n

// Apply it
{vp};
33

// Change the variable used for vectoring
m = 1;

// Apply the path again. Now 22 is returned
{vp};
22

// Declare a substitution path. At the moment $stack.x does not exist
any sp = path(g.h.{x}.n);
$stack.g.h.{$stack.x}.n

string x = "a2";

// Now we've got $stack.x apply the path.
{sp};
22

// Change x and apply the path again. Because the substitution was not
// resolved when the path was created the new value of x is honoured
x = "a1";
{sp};
11

// Perform a similar test, this time with the substitution resolved
// when the path is declared.
string y = "a3";
any sp = path(g.h.{y}.n);
$stack.g.h.a3.n

// Apply it
{sp};
33

// Change y and apply the path again. There's no change - we still get 33.
y = "a1";
{sp};
33

// Finally, note that it is an error if a path substution, when applied,
// does not resolve
any sp = path(g.h.{z}.n);
{sp};
file:/C:/.../pathtests.inq
com.inqwell.any.AnyRuntimeException: Substitution did not resolve {$stack.z}
      at com.inqwell.any.LocateNode$ResolvePathComponent.visitFunc(LocateNode.java:1091)
      at com.inqwell.any.AbstractFunc.accept(AbstractFunc.java:108)
      at com.inqwell.any.LocateNode$LocateAction.resolvePathItem(LocateNode.java:563)
      at com.inqwell.any.LocateNode$LocateAction.visitMap(LocateNode.java:649)
      at com.inqwell.any.AbstractMap.accept(AbstractMap.java:75)
      at com.inqwell.any.LocateNode.apply(LocateNode.java:382)
      at com.inqwell.any.LocateNode.doLocate(LocateNode.java:366)
      at com.inqwell.any.LocateNode.exec(LocateNode.java:199)
      at com.inqwell.any.AbstractFunc.execFunc(AbstractFunc.java:86)
      at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:112)
      at com.inqwell.any.EvalExpr.evalFunc(EvalExpr.java:149)
      at com.inqwell.any.parser.Inq.main(Inq.java:516)
file:/C:/.../pathtests.inq <parser>(54)

Inq done

The path function is used in the add function, covered in the section on Events, and when specifying GUI rendering information.