Echo
Module and Manifest
If you’ve gone through the quick start, you should have already created your first project. In that case, simply update the code in main.neva
to include the Echo
component from this example. For everyone else, let’s execute the following command:
neva new test
With these command, we’re creating a module. We’ll learn more about modules later, but for now, remember that any Nevalang program consists of at least one module.
Each module has a neva.yml
file, which describes its dependencies (modules can depend on other modules, in this case, there are no dependencies) and the required version of the compiler (in this case, 0.0.1). Such a file is called the module’s manifest.
So, after our module is created, replace the content of the test/src/main.neva
file with the following code:
import { std/builtin }
component Main(start any) (stop any) {
nodes {
reader builtin.Reader
printer builtin.Printer<string>
}
net {
:start -> reader:sig
reader:data -> printer:data
printer:sig -> :stop
}
}
Now make sure that you’re in the test
directory and run neva run src
. Terminal should block until you type something. Type anything e.g. “how are you?”. If everything is okay you should see this output:
> how are you?
The program printer what you’ve entered and quit. That’s all it does.
Packages, Std Module, Builtin Package
Let’s pay attention to this line:
import { std/builtin }
Previously, it was mentioned that our module has no dependencies, but that’s not entirely true. Every module implicitly depends on the standard library module - std
.
We’ve already discovered that Nevalang programs consist of modules, but what do modules consist of? Modules consist of packages. In this case, we import the builtin
package from the std
module to reuse it in our code.
Nodes, Component Instances
Let’s now return to our Main
component and see how it has changed. As we remember, it previously did nothing. Nevertheless, it had a body. Now, as we can see, its body has grown and consists not only of a net
but also contains a nodes
section.
nodes {
reader builtin.Reader
printer builtin.Printer<string>
}
Previously, it was mentioned that components send messages to each other. However, in reality, it’s not the components that exchange messages but their instances. These instances are called nodes and the components are merely “blueprints” from which such nodes can be created. A component effectively describes a node - its input-output ports and its behavior.
The network of any component that performs some work will inevitably consist of instances of other components. This means that a component typically depends on other components.
In our example, the Main component creates 2 nodes - printer
, an instance of the component builtin.Printer
, and reader
, an instance of the component builtin.Reader
.
This reader builtin.Reader
syntax should be understood as <node_name> <instantiation_expression>
, where the latter, in turn, is <package_name>.<component_name>
.
Type Parameters
Another syntactic construct that we should examine before moving forward is “generics,” or, as they are more academically called, type parameters.
Let’s look again at the declaration of the printer
node, specifically at its right part - the component instantiation:
builtin.Printer<string>
The construction <string>
immediately catches the eye. What does it mean? If we take a moment to look at the declaration of the builtin.Printer
component, we will see the following:
#extern(LinePrinter)
pub Printer<T>(data T) (sig T)
We will not yet touch upon what #extern
and pub
mean, as we still have to understand these. Let’s focus on the interface. We see that the Printer
component has an input port data
with type T
and an output port sig
also with type T
. But what is this type T
?
It’s all about this code Printer<T>
. Such a syntactic construction, where triangle brackets follow the component’s name, and within them are letters (typically uppercase), is called type parameters. In this case, the Printer
component has one parameter T
. Essentially, this means that when instantiating this component, that is, when creating a node based on it, we need to provide a type argument.
In our case, we provide string
and
Printer<T>(data T) (sig T)
Transforms for us into
Printer(data string) (sig string)
Type parameters are a mechanism that allows writing generic code. Without them, we would have to have a multitude of Printer variations for different data types. By specifying that the printer needs to work with strings (about them, and about other data types, we will also talk later), we get a safe way of using this node in our network.
Are you still here? It’s quite a lot for an introductory lesson, isn’t it? But there’s nothing to be done, our task is to delve into and understand how it works. Either way, we’re almost finished, there’s just a little bit left.
Connections, Senders, Receivers and Port Addresses
Let’s finally take another look at the network of our Main component:
net {
:start -> reader:sig
reader:data -> printer:data
printer:sig -> :stop
}
Now that we know what nodes are, we can understand this syntax a bit deeper. So, the network consists of connections. In this case, three. The order in which connections are declared in the code is absolutely not important. Remember - we do not control the flow of execution, but merely set the direction in which data flows.
Each connection consists of a sender and a receiver. Both are described by constructs called port addresses which in turn consist of a node and a port. For example, in the connection:
reader:data -> printer:data
The output port data
of the reader
node is directed into the input port data
of the printer
node. Ports to the left of ->
are always output, and ports to the right are always input.
IO Nodes
Finally, the curious reader might wonder, what about port addresses without specified nodes like :start
and :stop
?
The fact is that there are indeed two types of nodes - component instance nodes and the so-called IO nodes, of which there are always two in the network of each component - the in
node and the out
node.
Now, the in
node contains only output ports, and the out
node only input ports. This inversion might be confusing, but it is actually quite natural - a component reads data from its input ports as if they are the output ports of some node and correspondingly writes to its output ports as if they are someone else’s input ports.
Because you don’t have to think about it most of the time, Nevalang offers a shorter syntax for connections with IO nodes - :<port>
instead of <io_node>:<port>
. For example, in:start
is the same as :start
. One might ask, ‘But what if an input port and an output port have the same name?’ Well, you always have senders on the left and receivers on the right, so :x -> :x
means in:x -> out:x
.
Reader, Printer and The Algorithm
Finally, let’s dissect the algorithm our network executes. So, we have 3 connections:
:start -> reader:sig
reader:data -> printer:data
printer:sig -> :stop
As we see, the start
signal goes to the reader
node into the sig
port. The sig
port typically signifies a signal to start performing work. The Reader
component is designed in such a way that upon receiving this signal, it will block, waiting for input from the keyboard. After the user enters text and presses Enter, the program will be unblocked, and the entered data will be sent to the Printer
component, which, in turn, will print it out and emit a sig
signal on its output. We use this signal to close our loop, forming a cycle. The program will terminate if “ctrl+c” is pressed, but until then, it will continuously operate, constantly waiting for input and then printing it, ad infinitum.
Implicit Builtin
Before we move on, let’s simplify our program just a bit. We’ll remove the import { std/builtin }
line and also eliminate every builtin.
prefix from our nodes’ instantiations.
component Main(start any) (stop any) {
nodes {
reader Reader
printer Printer<string>
}
net {
:start -> reader:sig
reader:data -> printer:data
printer:sig -> :stop
}
}
It still works! In fact, the compiler implicitly injects the std/builtin
import into every file and checks if the entity we refer to is defined there. However, if we, for example, define our own Reader
in this package, it will shadow the built-in one.
What’s Next?
Well then, the Nevalang programmer is almost ready! Perhaps it’s time to tackle a real problem. In the next chapter, we’ll write a hello world!