3. Meta-Nodes

A meta-node is a function, of one or more arguments, which returns a value. Meta-nodes are nodes, themselves, however without a runtime node object. For the most part you can treat meta-nodes as ordinary nodes, e.g. you can set meta-node attributes using the same /attribute declaration. Referencing the value of a meta-node references the meta-node function.

Meta-node identifiers reside in the same namespace as that of ordinary nodes, that is you cannot have both an ordinary node and meta-node with identifier f. If there is a meta-node f, the node expression f references the meta-node function.

[Note]

Functor nodes with the meta-node as the operator are referred to as instances of the meta-node.

[Tip]

Meta-nodes are referred to as meta-nodes, since they are nodes which describe how to compute the value of their instance nodes. Meta-nodes may also be macro-nodes which are evaluated at compile-time, with the result being interpreted as Tridash code.

3.1. Defining Meta-Nodes

Meta-nodes are defined using the special : definition operator which has the following syntax:

Definition Operator Syntax. 

name(arg1, arg2, ...) : {
    declarations*
}

The meta-node identifier, name, appears on the left-hand side of the : operator followed by the comma-separated list of arguments in parenthesis. Each item, at position n, of the argument list is the identifier of the local node to which the nth argument is bound.

[Caution]

Identifiers beginning with /, followed by an alphanumeric character, are reserved for special operators. A meta-node cannot have the same identifier as a special operator. Currently no warning or compilation error is triggered, if the identifier is a reserved identifier but does not name an existing special operator, however that may change in a future release.

The body consists of a sequence of ordinary node declarations enclosed in braces { ... }. The braces are simply a way of grouping multiple declarations into a single expression, See Section 1.3, “Node Lists”. If the body of the meta-node contains just a single expression, the braces may be omitted.

The meta-node function returns the value of the last node in the body.

Example. 

# Returns 1 + `n`

1+(n) : n + 1

Factorial Example. 

# Computes the factorial of `n`

factorial(n) : {
    case (
         n > 1 : n * factorial(n - 1),
         1
    )
}

The following example demonstrates that the body can contain any valid node declaration:

Fibonacci Example. 

fib(n) : {
    fib(n - 1) -> fib1
    fib(n - 2) -> fib2

    case (
        n <= 1 : 1,
        fib1 + fib2
    )
}

[Important]

Meta-nodes must be defined before they can occur as operators in functors.

[Important]

Meta-node bodies are only processed after all global (or the scope in which the meta-node declaration occurs) declarations in the same file have been processed. This allows a meta-node g to be used within the body of another meta-node f even if the definition of g appears after the definition of f. Effectively this allows for mutual recursion.

Optional Arguments

A node in the argument list, of a meta-node definition, may also be of the form name : value. This designates that the argument, which is bound to local node name, is optional. If it is omitted, in an instance of the meta-node, the local argument node is set to value instead.

[Note]

value is processed in the global scope as the meta-node definition is processed. Thus value cannot (as of yet), refer to the preceding argument nodes of the meta-node.

The value may be omitted, written in prefix form :(name), in which case if the argument is omitted, the local argument node is set to a failure, see Section 2.6, “Failures”, of type No-Value.

Example. 

# Increment `x` by 1 or given delta

inc(x, d : 1) : x + d

# Increment `a` by default delta 1
inc(a)

# Increment `b` by explicit delta 2
inc(b, 2)

[Important]

Optional arguments may only be followed by optional arguments or a rest argument. An optional argument may not be followed by a required argument.

Rest Arguments

The last node in the argument list, of a meta-node definition, may also be of the form ..(name). This designates that the local node name is bound to the list containing the remaining arguments, on which the meta-node is applied, after the last optional or required argument. This allows for a variable number of arguments.

Example. 

# Add `n` to each remaining argument

add-n(n, ..(xs)) : {
    inc(x) : x + n
    map(inc, xs)
}

See Section 5.11, “Lists” for the documentation of lists and the list processing functions.

Local Nodes

Nodes local to the meta-node’s definition may only be referenced from within the definition itself even if they have the same identifiers as global nodes. Local nodes are created for each of the argument nodes.

A node reference, within the definition of a meta-node, primarily refers to the local node. If there is no local node with that identifier, it refers to the node in the enclosing scope. If the enclosing scope does not contain a node with that identifier, the scope’s enclosing scope is searched until the global scope is reached. If the node is not found in any enclosing scope a compilation error is triggered.

Local nodes are created if they appear as the target of a binding, whether implicit or explicit. This is the means by which local nodes, storing intermediate results are created. Local nodes are also created for each top-level atom node declaration, See Section 1.1, “Atom Nodes”.

[Note]

The node creation rules inside meta-node definitions differ from the node creation rules at the global scope.

[Tip]

A global node, with the same identifier as a local node, can be referenced using the outer .. operator.

Example: Local Nodes. 

a + b -> x
x + y -> n

addx(n) : {
    # `n` refers to the local argument node `n`, not the global `n`
    # `x` refers to the global node `x`
    n + x
}

Example: Meta-Nodes. 

1-(n) : n - 1

factorial(n) :
    case (
        # The `1-` refers to the global `1-` meta-node
        n > 1 : n * factorial(1-(n)),
        1
    )

Example: Local nodes storing intermediate results. 

x + 1 -> next

factorial(n) :

    # A local node `next` is created since it appears as the target of
    # a binding. `next` does not refer to the global node of the same
    # name.

    n - 1 -> next

    case (
        n > 1 : n * factorial(next),
        1
    )

Example: Local Node Declarations. 

cycle(a, b) : {
    x; y;  # Declare local nodes `x` and `y` 1

    cons(a, y) -> x
    cons(b, x) -> y

    x
}

1

Top-level atom node declarations, resulting in the creation of local nodes x and y.

Self Node

The special self node is a local node which represents the meta-node’s value. This node can be used to set the value, returned by the meta-node, using explicit bindings.

When an explicit binding to self is established, the meta-node no longer returns the value of the last node in its definition.

[Caution]

A meta-node may not have more than a single context, see Section 2.5, “Contexts”, as it is ambiguous which context’s value function to use as the meta-node function.

[Note]

In the absence of an explicit binding to self, the last node in the meta-node’s definition is implicitly bound to self.

Example. 

factorial(n) : {
    n * factorial(n - 1) -> next
    case (n > 1 : next, 1) -> self 1
}

1

Explicit binding to self.

In the example, above, the value returned by the factorial meta-node is set by an explicit binding to the self node. The meta-node no longer evaluates to the value of the last node in the declaration list.

The self node is particularly useful for creating a dictionary of values to which the meta-node evaluates to, see Section 2.9, “Subnodes”:

Example: Creating Dictionaries. 

Person(first, last): {
    first -> self.first-name
    last -> self.last-name
}

Nested Meta-Nodes

The body of a meta-node can contain other meta-node definitions nested inside it. These meta-nodes are local to the body, and can only be used inside it, even if the same meta-node identifier appears in an expression outside the body. If a meta-node with the same identifier is already defined at global scope, the nested meta-node shadows it in the scope of the body. This means that references to the meta-node within the body refer to the nested meta-node and not the global node.

Example: Factorial with Nested Tail-Recursive Helper Meta-Node. 

factorial(n) : {
    # `iter` is local to `factorial`
    iter(n, acc) : {
        case (
            n > 1 : iter(n - 1, n * acc),
            acc
        )
    }

    iter(n, 1)
}

3.2. Recursive Meta-Nodes

Meta-nodes may be recursive and mutually recursive, i.e. when a meta-node f contains an instance of another meta-node g in its definition, and g contains an instance of f in its definition.

Each call to a meta-node consumes an amount of stack space. Further calls, within the meta-node, increase the amount of stack space if they are strictly evaluated. However, if a call to a meta-node is conditionally evaluated, i.e. lazily, it does not increase the amount of stack space used, since a thunk is returned, rather than the final result, thus freeing the amount of stack space used by the current call. See Section 2.4, “Evaluation Strategy”.

The following are examples of meta-nodes in which one or more of the arguments are evaluated lazily:

  • In the if meta-node, from the core module, the if-true and if-false arguments are evaluated lazily since only one of the arguments is actually evaluated, depending on the value of the first test argument. The test argument is evaluated strictly as its value is always required in order to compute the return value of the meta-node.

    if(test, if-true, if-false)
  • In the and and or meta-nodes, from the core module, the first argument is strictly evaluated however the second is lazily evaluated, as whether it is actually evaluated depends on the value of the first argument.

    and(a, b)
    or(a, b)

3.3. Outer Node References

The value of a node, declared in the global scope, can be referenced from within a meta-node, either directly by its identifier, as described in Local Nodes, or with the outer node reference operator (..). This is a special operator which takes a node identifier as an argument and searches for a node with that identifier, in each enclosing scope, starting from the scope in which the meta-node is defined. The first node found is referenced.

[Note]

It is not necessary for the node to have been declared prior to the meta-node definition, as meta-node definitions are only processed after all declarations in the source file have been processed. However, in general the node should be declared in the same source file.

Example. 

n

# ..(n) references the global node `n`
addn(n): n + ..(n)

Referenced outer nodes, whether implicitly or by the .. operator, are treated as additional hidden arguments, that are added to the argument list of each instance of the meta-node. The result is that any change in the values of the referenced nodes, will trigger a value update in each instance of the meta-node.

The previous example can be thought of as:

# Not valid syntax.

# Illustrates that outer node references are equivalent to additional
# arguments.

addn(n, ..(n)) : n + ..(n)

Thus the value of n is appended to the argument list of all instances of addn, e.g. addn(node) becomes addn(node, n).

Meta-nodes reference all outer nodes referenced by the meta-nodes which are used in their body. In the previous example, if a meta-node makes use of addn, it will also reference the node n declared in the global scope.

[Important]

Whilst the value of an outer-node can be referenced from within the body of a meta-node, bindings with the node as the target cannot be established, from within the body of the meta-node.

3.4. External Meta-Nodes

External meta-nodes are meta-nodes without a definition, which are used to invoke functions defined outside of Tridash code. The special /external declaration creates a meta-node without a definition.

Syntax. 

/external(id, args...)

id

The meta-node identifier

args

The argument list

The argument list has to be provided in order for the arity of the meta-node to be known. The same rules apply for external meta-node argument lists as for ordinary meta-node argument lists. Symbols designate required arguments, arguments of the form :(arg, value) designate optional arguments and ..(rest) designates a rest argument. The argument identifiers, however, do not name local nodes.

An external definition for the meta-node has to be provided, and linked with the generated code. In the JavaScript backend, instances of the meta-node are compiled to a call to the JavaScript function with the name given by the value of the js-name attribute. If the js-name attribute is not set, the result is a compilation error indicating that the JavaScript backend does not support the external meta-node.

3.5. Higher-Order Meta-Nodes

An atom node expression consisting of the meta-node itself references the meta-node’s function as a value. This function can be passed to other meta-nodes as an argument, or bound to another node.

In a functor expression, in which the operator is not a meta-node but is an ordinary node, the function stored in the node’s value is called. If the operator node does not evaluate to a function, the entire functor node evaluates to a failure of type Type-Error, see Section 2.6, “Failures”. If the function is invoked with more, or less, arguments than it expects,the functor node evaluates to a failure of type Arity-Error

Example: Binding Meta-Node to other Nodes. 

inc(x) : x + 1

inc -> f   1
f(a) -> x  2

1

Value function of inc meta-node bound to f node.

2

Function stored in f meta-node applied on argument a.

See Section 3.3, “Outer Node References” for an example in which a meta-node is passed as an argument to another meta-node.

The function of a meta-node which does not have optional arguments or outer nodes is effectively a constant, as is the case with the inc meta-node in the example above. If, however, the meta-node references outer nodes, a reference to the meta-node’s function also references the values of the outer nodes. As such, if a node is bound to the meta-node’s function, a binding between the outer nodes and the node is also established.

Example: Reference Meta-Node Function with Outer Nodes. 

# Increments `x` by the global `delta`
inc(x) : x + delta

inc -> f
f(a) -> x

In the example, above, node f is bound to the value function of inc. However, since inc references the global delta node, a binding between f and delta is also established. The value function of f creates a function which invokes the inc with the value of delta. As a result, when the value of delta changes, the value of f is recomputed, and likewise the value of f(a) is recomputed.

The same semantics apply for optional arguments with default values which are not constant literals.

3.6. Macro Nodes

A macro-node is a meta-node which is evaluated at compile-time with the result interpreted as a Tridash node declaration.

A meta-node is marked as a macro-node by setting the macro attribute to true. Once set, the meta-node’s function will be evaluated when each instance of the meta-node is processed. The arguments passed to the function are the raw argument node expressions of the functor node expression.

[Tip]

Attributes are set on meta-nodes in the same way as they are set for ordinary nodes. The macro attribute of a meta-node f is set to true, with the following declaration:

/attribute(f, macro, True)

Atom node expressions are represented by a special symbol type and functor node expressions are represented as a list with the operator in the first element of the list.

The return value of the meta-node function is processed as though it is a parsed node declaration appearing in source code.

Literal Symbols

The special /quote operator returns its argument, treated as a literal symbol rather than a node expression.

[Tip]

The ' macro from the core module is the preferred shorthand for the /quote operator.

Example. 

# This is interpreted as the literal symbol `x` rather than the node
# with identifier `x`.

/quote(x)

# The following is a shorthand for the above
'(x)

These can be used inside macro nodes to insert literal node or operator names.

Example: Definition of ' macro. 

'(thing) :
    list(/quote(/quote), thing)

/attribute(', macro, True)

Node References

Generally a macro-node expands to a declaration involving some other meta-node. The meta-node might not be located in the same module, see Section 4, “Modules”, as the module in which the macro-node instance occurs. Using the quote operator to generate a declaration involving the meta-node may result in a compilation error, if the meta-node is not present in the module in which the macro-node instance occurs, or may result in a node declaration involving an entirely different meta-node, if the module contains a node with the same identifier.

Node objects can be referenced directly with the node reference operator, &. When the declaration returned by a macro-node contains a raw node object, no node lookup is done and the raw node object is used as though it has been returned by node lookup. This is useful in macros as the node is looked up once in the module containing the macro-node’s definition.

Example: Definition of <- Macro. 

<-(target, src) :
    list(&(->), src, target)

/attribute(<-, macro, True)

The <- macro function, in the example above, returns a functor expression where the operator is the node object ->. When the functor expression is processed, the operator is taken to be the -> node, rather than the node with identifier -> in the module in which the instance is processed.

Any node can be referenced including ordinary nodes and macro-nodes. Special operators, however, cannot be referenced and have to be returned as quoted symbols instead. There is no issue with directly quoting the special operator’s identifier, in expressions returned by macros, as there is for meta-nodes since the meaning of a special operator cannot be overridden and does not change with the module. Most of the special operators mentioned till this point, which are not an identifier prefixed with /, such as ->, :, &, ., .. are actually builtin macro nodes which expand to an internal special declaration, thus can be referenced with the & operator. Special operators beginning with /, such as /attribute, /operator, /module are actual special operators and cannot be referenced with &.

When a raw node referenced occurs in code which is intended to be evaluated at runtime, rather than during macro expansion, the runtime node object, of the node, is referenced. The nature of this object is dependent on the backend.

3.7. Instances as Targets

By default, a meta-node instance appearing as the target of a binding, that is on the right hand side of the -> operator, will result in a compilation error. You may have noticed, however, that some meta-nodes in the core module, can also appear as targets of a binding, particularly to-int, to-real and to-string. This is achieved by setting the target-node attribute.

The target-node attribute stores the meta-node, which is applied on the value of the meta-node instance, in order to compute the value of the arguments. When the target-node attribute is set, a binding is established between the meta-node instance, as the dependency, and each argument node, as the observer. The function of the binding context is set to the meta-node stored in the target-node attribute.

[Note]

The target-node meta-node is looked up immediately when the attribute is set, and in the same module in which the /attribute declaration is processed.

As an example consider a meta-node f with the target-node attribute set to g. A declaration of the form:

x -> f(arg)

results in the following binding also being established, alongside the main binding of arg -> f(arg):

g(f(arg)) -> arg
[Note]

The functor node g(f(arg)) is not created, rather f(arg) is bound to arg directly and g is set as the value function.

This is useful for creating invertable meta-nodes where instead of computing a result given the values of the argument nodes, the values of the argument nodes can be computed given the result. This is achieved by binding to the meta-node instance, with the target-node attribute set to the inverse function.

The to-int meta-node from the core module has its target-node attribute set to int. Thus the binding x -> to-int(y), will result in the value of y being set to the value int(x), on changes in the value of x.

[Caution]

In order for the bindings to the argument nodes, to be established, the /attribute declaration, which sets the target-node attribute, must occur before between the definition of the meta-node and its first instance.

3.8. Target Node Transforms

The target-node attribute allows for a binding of a simple function to be established in the reverse direction, from the meta-node instance to its arguments. However, it lacks the functionality for setting a different function for each argument or generating more complex binding declarations.

The target-transform attribute allows a meta-node to be set as the function which is called whenever an instance of the meta-node appears as the target of a binding. The function is called with two arguments: the source node of the binding and the functor expression, which appears as the target of the binding. The function should return a declaration which is processed instead of the binding declaration. The result is processed as though it appears at top-level and unlike with a macro-node, the result is not substituted directly in the place of the meta-node instance.

[Note]

The source argument is not necessarily the actual source node declaration but is generally an atom node, with a randomly generated identifier, which should serve as the source node for the binding declarations generated by the target-transform node.