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.
Functor nodes with the meta-node as the operator are referred to as instances of the meta-node. |
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. |
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.
Identifiers beginning with |
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 ) }
Meta-nodes must be defined before they can occur as operators in functors. |
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 |
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.
|
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)
Optional arguments may only be followed by optional arguments or a rest argument. An optional argument may not be followed by a required argument. |
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.
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”.
The node creation rules inside meta-node definitions differ from the node creation rules at the global scope. |
A global node, with the same identifier as a local node, can be
referenced using the outer |
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` cons(a, y) -> x cons(b, x) -> y x }
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.
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. |
In the absence of an explicit binding to |
Example.
factorial(n) : { n * factorial(n - 1) -> next case (n > 1 : next, 1) -> 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 }
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) }
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)
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.
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.
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. |
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...)
|
The meta-node identifier |
|
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.
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 f(a) -> x
Value function of | |
Function stored in |
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.
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.
Attributes are set on meta-nodes in the same way as they are set for
ordinary nodes. The /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.
The special /quote
operator returns its argument, treated as a
literal symbol rather than a node expression.
The |
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)
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.
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.
The |
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
The functor node |
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
.
In order for the bindings to the argument nodes, to be
established, the |
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.
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 |