Source Code

This section provides an overview of Nevalang, focusing on its user and compiler perspectives, excluding the type-system which is covered separately. It doesn’t delve into the execution details of Nevalang programs, but rather explores the abstractions present in the source code and their governing principles.

Build

Build is set of Nevalang modules. Every module has unique module reference. One of the modules is entry module.

Module

Module is a set of packages. Every module has its own manifest file.

Entry Module

Entry module is a root module for compilation. Every entry module must have at least one executable package.

Module Manifest

File that describes which version of language this module supports and list of its dependencies.

Module Dependencies

Every module except std has dependencies, at least std. Module defines dependencies by specifing dependend module’s path and version. Every dependency module can have local alias.

Package

Package is a set of files located in the same directory. Name of the package is the path to its directory from module’s root. All entities in a package forms single namespace so they must have unique names across package. An entity can refer to entities described in other files in the same package without imports.

Executable Package

Package without public entities and with main component

File

File is a set of imports and entities. Unlike package file is not a namespace itself, but imports declared inside one file are not visible inside another. There’s no restriction on how one should group entities in files inside a package.

Imports

Imports allow to use entities defined in other packages. Imports declared in one file are not visible inside another. Import consist of module reference and package name. E.g. std:http/net is an import of package http/net from module std. Only public entities can be imported.

Entities

Entities are abstractions for creating programs. They are either private or public. They are private by default and can be made public by using pub keyword. Every entity has name that is unique across the package. Entities are referenced by entity references.

There are four kinds of entities (from simple to complex):

  1. Type
  2. Interface
  3. Constant
  4. Component

Entity Reference

Entity reference consist of an optional package name and name of the referenced entity. Package name can be omitted if entity that we reference either exist in the same package or in std/builtin. If entity in current package has the same name as the one in the builtin, then it shadows it.

Type Entity

Type entity (type definition) consist of an optional list of type parameters followed by optional type expression that is called body.

Base Type

Type definition without body means base type. Compiler is aware of base types and will throw error if non-base type has no body. Base types are only allowed inside std/builtin package. Some base types can be used inside recursive type definitions.

Recursive Type Definition

If type refers to itself inside its own definition, then it’s recursive definition. Example: type l list<l>. In this case list must be base type that supports recursive definitions. Compiler knows which types supports recursion and which do not.

Type Parameters (Generics)

Every type paremeter has name that must be unique across all other type parameters in this definition and constrant.

Type Parameter Constraint

Constraint is a type expression that is used as supertype to ensure type compatibility between type argument and type corresponding parameter. If no constrained explicitly defined then any is implicitly used.

Type Parameters and Arguments Compatibility

Argument A compatible with parameter P if there’s subtyping relation of form A <: C whereC is a constraint of P. If there’s several parameters/arguments, every one of them must be compatible. Count of arguments must always be equal to the count of parameters.

Type Expression

There is 2 kinds of type expressions:

  1. Instantiation expressions
  2. Literals expressions

Type expressions can be infinitely nested. Process of reducing the type expression called type resolving. Resolved type expression is an expression that cannot be reduced to a more simple form.

Type Instantiation Expression

Such expression consist of entity reference (that must refer to existing type definition or type parameter) and optional list of type arguments. Type arguments themselves are arbitrary type expressions (they follows the same rules described above).

Literal Type Expression

Type expressions that cannot be described in a instantiation form.

Interface Definition

Interface is a component signature that describes abstract component - its input and output ports and optional type parameters. Interfaces are used with dependency injection and abstract components.

Ports

Port definition consist of a type expression describing the data-type port expects and a flag that describes whether the port is an array or single port. Type expression can refer to interface’s type parameters. If no type paremeter given then any is implicitly used.

Single Ports

Single port is port with one slot. Reference to such ports should not include slot index.

Array Ports

Array port is port with multiple (up to 255) slots. Such ports must be referenced either via slot indexes or in array-bypass connection expressions.

Constant

Constant is an entity that consist of either message or entity reference to other constant. Message can include references to other constants. Constant messages can be infinitely nested. Constants may refer imported constants from other packages. Components are only entities that can refer constants, that are not constants themselves - they can refer to constants via compiler directives and from their networks.

Component

Component always has interface and optional compiler directives, nodes and network. There are two kinds of components: normal and native ones.

Main Component

Executable package must have component called Main. This component must follow specific set of rules:

  • Must be normal
  • Must be private
  • Must have exactly 1 inport start
  • Must have exactly one outport stop
  • Both ports must have type any
  • Must have no abstract nodes

Main component doesn’t have to have network but it is usually the case because it’s the root component in the program.

Native Components

Component without implementation (without nodes and network) must use #extern directive to refer to runtime function. Such component called native component. Native components normally only exist inside std module, but there should be no forced restriction for that.

Normal Component

Normal component is implemented in source code i.e. it has not only interface but also nodes and network, or at least just network. Normal components must never use #extern directive.

Nodes

Nodes are things that have inports and outports that can be connected in network. There’s two kinds of nodes:

  1. IO Nodes
  2. Computational nodes

IO nodes are created implicitly. Every component have one in and one out node. Node in has outports corresponding to component’s interface’s inports. And vice versa - out node has inports corresponding to component interface’s inports.

Computational nodes are nodes that are instantiated from entities - components or interfaces. There’s 2 types of computational nodes: concrete and abstract. Nodes that are instantiated from components are concrete nodes and those that instantiated from interfaces are abstract nodes.

Interfaces and component’s interfaces can have type parameters. In this case node must specify type arguments in instantiation expression.

Dependency Injection (DI)

Normal component can have abstract node that is instantiated from an interface instead of a component. Such components with abstract nodes needs what’s called dependency injection.

I.e. if a component has dependency node n instantiated with interface I one must provide concrete component that implements this interface.

Dependency Injection can be infinitely nested. Component Main cannot use dependency injection.

Component and Interface Compatability (Implementation)

Component implements interface (is compatible with it) if type paremeters, inports and outports are compatible.

Type parameters are compatible if their count, order and names are equal. Constraints of component’s type parameters must be compatible with the constraints of the corresponding interface’s type parameter’s constraints.

Component’s inports are compatible with the interface’s if:

  1. Amount is exactly equal
  2. They have exactly the same names and kind (array or single)
  3. Their types are compatible (are subtypes of) with the corresponding interface’s inports

Outports of a component are compatible with the interface’s if:

  1. Amount is equal or more (this is only difference with inports)
  2. Exactly the same names and kind
  3. Their types are compatible

Network

Network is a set of connections. Every connection consist of sender-side and receiver-side. Sender and receiver must be compatible. There is 2 types of connections: normal and array-bypass.

Normal Connection

Normal connection can have several types of sender-side and receiver-side.

Sender-side:

  1. Port address (traditional)
  2. Constant reference
  3. Primitive message literal

Receiver-side:

  1. List of inport-addresses
  2. List of deferred connections

Sender-side in of a normal connection can also have optional struct selectors.

Sender-Side Struct Selectors

If (resolved) type of sender-side is structure, then it’s possible to have selectors in it. Selectors are list of strings, where each element means field in a struct. More than one selector means that there is a structure inside structure. Selectors must be type safe. I.e. it must be possible to “unwrap” structure each time we process next selector.

Port Address

Port Address consist of name of the node, name of the port and optional index of the slot. Slot index must be present only if port address refers to array-port.

Constant Reference Sender

In normal connection not just port address but also reference to constant entity (that must be available in the scope) could be a sender. This works exactly like if there’s emitter sender with bound constant.

Primitive Message Literal Sender

This works almost like constant reference sender except instead of referencing some constant we simply use message literal. Only primitive data-types are supported: booleans, numbers, strings and enum members.

Array-Bypass Connection

Connection that connects all slots of some sender with all slots of some receiver. Sender and receiver must both be array-ports. Component is only allowed to bypass it’s own inports. Such connection always consist of two port addresses without slot indexes.

Bound Constant

Constant that is referenced inside bind compiler directive

Compiler Directive

Special instructions for compiler. Directives that must be supported by the compiler are #extern, #autoports and #bind.

Runtime Function Overloading

Native components pass several arguments to #extern directive to utilize overloading. In this case arguments are pairs separated by whitespace, they have form #extern(t1 f1, t2 f2, ... tN fN) where t is a type and f is the name of a runtime function.

Component that uses overloading must have exactly one type parameter (it’s name doesn’t matter) of type union. Types that are referenced inside directive must be members of that union.