It's been 4 months since the last release, with major changes across all parts of the language - from syntax to runtime implementation, including new features, stdlib components, and bug fixes.
Changes in Existing Features
def
Keyword
You are now must use def
keyword to define components.
def Main(start any) (stop any) {
:start -> :stop
}
The word def
is short and common (used in Python, Ruby, Clojure, Scala, Elixir, Nim, Crystal, Groovy) and it means "define," which has a broader meaning. The word flow
led to confusion because the abstraction is called "component." But thanks to @ajzaff for the suggestion anyway
Deferred Connections
Due to the addition of binary and ternary expression senders, we had to reserve ()
and select a different syntax for deferred connections.
Before
:start -> (42 -> println)
After
:start -> { 42 -> println }
Struct Selectors
Structs are now their own type of sender, rather than a modifier for other senders.
Before
someStruct.someField -> println
After
someStruct -> .someField -> println
Why?
This allows for implementing struct selectors on the receiver side in connections with multiple receivers.
s -> [
.field1 -> r1
.field2 -> r2
]
Note that you don't have to write
foo -> .bar -> .baz -> ...
; you can instead writefoo -> .bar.baz -> ...
New Language Features
Range Expressions
You are now allowed to use syntax sugar for using Range
component
:start -> 1..100 -> ...
Range is a type of sender, so it might be used anywhere, where any other sender is expected (with the respect of type-safety, ofcourse)
Will emit stream<int>
of messages from 1
to 99
(range is exclusive)
Binary Operators
You can now write binary expressions as you would in other languages.
(1 + 2) -> println
Please note that you must use parentheses, as Nevalang currently lacks operator precedence. For now, nested binary expressions look like this:
((1 + 2) * 3) -> println
Supported Operators
14 binary operators are implemented: 6 arithmetic, 6 comparison, and 2 logical.
+ - * / % **
== != > < >= <=
&& ||
How It Works
Syntax is simple:
(sender_1 OPERATOR sender_1)
sender_1
and sender_2
can be any valid senders: port addresses, constant references, message literals, other binary expressions, etc.
The compiler understands these expressions and desugars them into regular connections using stdlib components. You can check builtin/operators.neva
to see their API. They all follow the same pattern:
def Op(left T1, right T2) (res T3)
Both operands must be compatible with their operator, or the compiler will throw an error.
A note on Reduce
The higher-order component Reduce
has been modified, and the interface for the reducer now looks like this
interface IReduceHandler<T, Y>(left T, right T) (res Y)
As you may have noticed, many binary operators are actually reducers. Thanks to @ajzaff for this idea.
Ternary Operator
Similar to binary expressions, ternary expressions use the ? :
operator, as in most languages.
(condition ? ifValue : elseValue) -> ...
All 3 parts are senders, so all sender types are supported, including other ternary and binary expressions. The condition sender must emit bool
, and the If and Else parts must both be compatible with the receiver, otherwise the compiler will throw an error.
Similar to binary operators, the ternary operator is just syntactic sugar for using the Ternary
component:
def Ternary<T>(if bool, then T, else T) (res T)
For both binary and ternary expressions, you may explicitly use operators as components if the expression-based syntax doesn't cover your case.
Switch
statement
Another type of sender was added to simplify the use of the Switch
component. This is necessary when you need to trigger different branches of the network based on the value of an incoming connection. The syntax is as follows:
s -> switch {
c1 -> r1
c1 -> r2
c1 -> r3
_ -> r4
}
Here s
means sender, which could be any sender (including binary and ternary expressions). c1, c2, c3
are "case senders" - they are also senders, and any senders will work as long as they are type-safe. Finally, _
is the default sender. The default branch is required, making each switch expression exhaustive. The compiler ensures that the incoming s ->
and all c
and _
senders are compatible with their corresponding receiver parts.
If one branch is triggered, other branches will not be (until the next message, if the corresponding pattern fires) - one way to think about this is that every branch has a "break" (and there's no way to "fallthrough").
I don't like this explanation because it's control-flow centric, while Nevalang's switch is pure dataflow, but it makes sense as an analogy.
The switch statement is syntactic sugar for the Switch
component:
def Switch<T>(data T, [case] T) ([case] T, else T)
You are allowed to use
Switch
as a component, if you need to, but prefer statement syntax if possible
To better understand the switch
statement, let's look at a few examples:
// simple
sender -> switch {
true -> receiver1
false -> receiver2
_ -> receiver3
}
// multiple senders, multuple receivers
sender -> switch {
[a, b] -> [receiver1, receiver2]
c -> [receiver3, receiver4]
_ -> receiver5
}
// with binary expression senders
sender -> switch {
(a + b) -> receiver1
(c * d) -> receiver2
_ -> receiver3
}
// nested
sender -> switch {
true -> switch {
1 -> receiver1
2 -> receiver2
_ -> receiver3
}
false -> receiver4
_ -> receiver5
}
// as chained connection
sender -> .field -> switch {
true -> receiver1
false -> receiver2
_ -> receiver3
}
Related to #725
Note on multuple senders/receivers
In this example
sender -> switch {
[a, b] -> [receiver1, receiver2]
c -> [receiver3, receiver4]
_ -> receiver5
}
If the sender
message is equal to either a
or b
, it will be sent to both receiver1
and receiver2
. You can also have multiple senders and one receiver, or one sender and multiple receivers.
Note on Pattern Matching (ROADMAP)
switch
is a "router," not a "selector." It can only redirect incoming messages to a branch based on a condition (equality comparison). But what if we want to select one of the possible options instead of redirecting the incoming message? For this, another component called match
will be implemented. It will work similarly to switch
but act as a selector rather than a router.
num -> match {
42: 'a'
43: 'b'
_: 'c'
} -> println
Note the outgoing
->
thatswitch
does not have.
WARNING: match
is NOT implemented yet.
Changes in Standard Library
Tap
component
Sometimes component needs to receive data, perform some action and pass that data further. However, due to impossibility to reuse same sender twice or more, it leads to need for explicit locks. Deferred connections do not cover this case. Explicit lock make network harder to reason about. Tap
is a higher-order component, that implements this logic for you. All you need to do, is to provide dependency node, thar receives data and sends a signal when finishes. Signal could be of any type, so no need for dealing with locks manually.
def Tap<T>(data T) (res T)
For example here we need to pass data to FirstLine
and then to SecondLine
, but FirstLine
only sends a signal, that it finished, so we need to lock data and send it further. This is tedious and error prone, so we can use Tap
to handle this case:
def Next2Lines(data int) (sig any) {
first Tap<int>{FirstLine}
dec Dec<int>
second SecondLine
---
:data -> first -> dec -> second -> :sig
}
New Runtime Design (race-free, simpler and much faster)
One of the biggest challenges for a long time was overcoming various race conditions and out-of-order delivery. Finally, a proper design has been found. As with all elegant solutions, its power lies in its simplicity, bringing not just predictability but also much better performance.
The new runtime design is "connectionless." After compilation, a Nevalang program consists of runtime functions passing messages through channels. There are no more intermediate message-passing goroutines that caused concurrency issues. This is achieved through graph reduction. After IR generation, we strip intermediate connections, leaving only runtime functions. The runtime also has a better API, making it easier than ever to work with runtime functions.
Issues solved with new design:
Please note that this doesn't mean your code will be free of race conditions. It means they are not related to the runtime implementation but rather to your program logic. There are still some issues at the stdlib level, such as #754.
Interpreter was removed
Generating Go and compiling it with the Go compiler is now the only way to run Nevalang programs. You don't have to do it manually; neva build
will call go build
under the hood, so you'll get a binary (unless you pass --target=go
). The command neva run
will also run the built executable and clean it up afterward. This change simplifies the project, as the interpreter required duplicated functionality that the compiler already had, but without the ability to reuse it. This takes away some possibilities, but we must choose a direction, and for Neva, it's compilation to Go.
Documentation in the Repository
Documentation now exists in the docs
folder at the root of the main Nevalang repository. The website documentation is deprecated until we implement generating HTML from markdown in the docs folder. The documentation has been updated, but some parts are already outdated due to the rapid changes in the language at this early stage. Please join our Discord server if you have any questions or problems.
Compiler Improvements
The compiler now handles more potential errors with static semantic analysis. Error messages have been improved, so in most cases, you should have the location of the error. Error messages are still far from perfect; they are a complex issue on their own, but we have some ideas on how to handle them properly. Several bugs were also fixed. Finally, many things were refactored to make the code cleaner.
Autogenerated Changelog
- STD: separate flow and logic builtin components by @emil14 in #694
- build(deps): bump golang.org/x/net from 0.17.0 to 0.23.0 by @dependabot in #633
- build(deps): bump github.com/cloudflare/circl from 1.3.3 to 1.3.7 by @dependabot in #631
- Connectionless by @emil14 in #696
- Dash delim syntax by @emil14 in #699
- Docs by @emil14 in #701
- feat(docs): WIP but improved dramatically by @emil14 in #702
- Docs2 by @emil14 in #703
- Docs2 by @emil14 in #705
- feat(docs:components) by @emil14 in #706
- Docs5 by @emil14 in #707
- Gobackend fix by @emil14 in #710
- Backend fix by @emil14 in #714
- Syntax impovements by @emil14 in #731
- Selectors rewrite by @emil14 in #733
- Lsp improvements by @emil14 in #736
- feat(parser): ternary by @emil14 in #738
- feat(docs:qna): ternary by @emil14 in #740
- feat(std): rename operators/reducers ports nameing from acc/el to lef… by @emil14 in #745
Full Changelog: v0.25.0...v0.26.0