Releases: nevalang/neva
v0.30.0
Cross-Compilation Support
Target Flags
You can now use target-os
and target-arch
flags to specify operational system and architecture when using neva build
command. This is only supported when --target=native
(or omitted, which is the same). You must always use these flags in pair or omit both of them, they cannot be used in separate
neva build --target-os=windows --target-arch=arm64 examples/hello_world
Osarch Command
New command neva osarch
lists all possible combinations of target-os
and target-arch
values. At the moment of publishing this release, the list is the following:
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
freebsd/riscv64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/loong64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/ppc64
openbsd/riscv64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
wasip1/wasm
windows/386
windows/amd64
windows/arm
windows/arm64
CLI Help Improved
This release closes #820 issue by improving the --help
flag and help
command (which are the same thing) by adding more information about build
command, especially using --target
(lists supported targets such as native
, go
, wasm
, json
and dot
) and corresponding --target-os
and --target-arch
flags
v0.29.1
v0.29.0
Nevalang is a general purpose dataflow programming language with implicit parallelism and static typing that compiles to Go and machine code:
What's New?
This is another big release. This time we ship 2 big things and a lot of small ones. Number one big thing is support for anonymous ports in interfaces and another is solving DI drilling problem
Interfaces With Anonymous Ports
// before
interface IDoer(data any) (sig any)
// after
interface IDoer(any) (any)
This allows to have interfaces with many more implementations and makes implementing an interface much simpler. All this leads to much better composability in the ecosystem.
This is however only supported for nodes with 1 inport and 1 outport.
DI Drilling
The name is similar to React's "props drilling" problem
import { fmt } // package with dependency (with interface implementation)
def Main(start any) (stop any) {
foo Foo{printer fmt.Println<any>} // dependency injected
---
:start -> foo -> :stop
}
interface IPrinter(any) (any) // anonymous ports, baby
def Foo(start any) (stop any) {
bar Bar{printer IPrinter} // dependency passed through
---
:start -> bar -> :stop
}
def Bar(start any) (stop any) {
printer IPrinter // dependency defined
---
:start -> printer -> :stop
}
Nevalang supports dependency injection natively almost from the beginning. However, it was very limited up to this point. It was impossible to have intermediate layer between dependency provider and dependency consumer.
For example, Filter
uses Split
inside. Split needs dependency IPredicate
(which is now any component that receives T and sends bool implements, thanks to anonymous ports interfaces feature!). And now you as a user can pass dependency to Filter
, so Filter can bypass it right into split.
This was fundamental limitation for several years of development and solving it was a bit step for the language.
WARNING: You have to explicitly reference name of the dependency at each layer (in this case "printer") because otherwise compiler won't figure out where defined dependency comes from. This might be and might not be a temporary limitation, but that's the way it is.
Nevalang supports anonymous dependency injection similar to how it supports anonymous ports for interfaces (oh yeah) now, so if you don't have DI drilling in your case (Dependency flow is 1 level from parent to direct child), then feel free to pass deps
Node{Dep}
. Remember it's only possible when there's 1 dependency so compiler don't have to guess
Everything Else
- Refactoring (stdlib) - renaming of ports of components for consistency
- Refactor (compiler) - irgen package doesn't use
compiler.Error
because any error at that step is internal - Bug fix (compiler) automatically trims trailing
/
withneva run
andneva build
- Bug fix (lsp) - language server doesn't lookup neva modules upper than current workspace
- Improvement (compiler) - parser saves location of each entity for better compiler errors
- Bug fix (stdlib) -
Filter
component is re-implemented and is available to use again and all its e2e tests are active. New implementation handles.idx
field of astream<T>
message properly (this caused a deadlock in specific programs) - ... and more!
This release closes following issues
v0.28.2
What's Changed
- Fixes in analyzer - add missing support for binary and ternary expressions inside
switch
- Tutorial fixes
Full Changelog: v0.28.1...v0.28.2
v0.28.1
What's Changed
Sub
operator fixed: left and right operands were confused- More e2e tests for operators added
- Tutorial fixed
Full Changelog: v0.28.0...v0.28.1
v0.28.0
What's New?
Compiler
- Error messages improved in several places, by using correct location
Stdlib
- New
fmt.Print
component that works likefmt.Println
but without the newline. - Outport of
http.Get
renamed fromresp
tores
for consistency with other stdlib components. - New
strings.ToUpper
andstrings.ToLower
components.
Documentation
- Book updated to match latest language changes
- Tutorial extended with new section about switch expressions
Full Changelog: v0.27.1...v0.28.0
v0.27.1
What's New?
This is small release that only contains bug-fixes, new tests and documentation updates.
Compiler Error Fixes
Special thanks @gavr123456789 for testing and feedback
- Only print deepest child error and ignore all the upper hierarchy until we figure out how to properly build context for errors.
- Fix error location for non-properly used ports, now location of the node is used, instead of the node's interface location
- E2E test was added to check that we won't broke error location for unused outport
Neva CLI
- Now generates hello world with chained connection instead of deferred one, because we support references and literals as chained now
Documentation
- Book was updated to match latest language changes
- Tutorial is extended with Dataflow section, still WIP
Autogenerated Changelog
- feat(tutorial): updated due to new syntax for constants (support for … by @emil14 in #775
- fix(docs,tutorial): typo cNina -> cHina by @emil14 in #776
- Readme upd3 by @emil14 in #784
- Readme upd3 by @emil14 in #785
- feat(version): 0.27.1 by @emil14 in #786
Full Changelog: v0.27.0...v0.27.1
v0.27.0
What's New?
After discord discussion a decision was made to increase priority for #717, this release fixes that issue.
Before you had to use deferred connections -> {}
feature (or explicit locks) almost always when you wanted to use constant references or literals as network senders. Example:
:start -> { 'Hello, World!' -> println -> :stop }
Now alternative syntax is supported:
:start -> 'Hello, World!' -> println -> :stop
It means that now constant references and literals are valid parts of the chained connections.
By the way, old variant is still supported, 1) deferred connections are needed not just to block constants 2) because constants must be used outside of chained connections (e.g. inside binary/ternary/switch/etc expressions).
From now on you should always prefer using this new syntax, instead of old one, because it's easier to read and it has better performance. However, there might be cases where ->{}
is exactly what you need.
How it Works?
Old variant
:start -> { 'Hello, World!' -> println -> :stop }
Hello World!
const was sent to println
in infinite loop from the program startup, but implicit lock
node, that was waiting for the signal from start
port, was inserted between then. Important note is constant sender was not waiting for anything and instead we had to defer receiving event, to guarantee that we will print exactly once.
New variant
:start -> 'Hello, World!' -> println -> :stop
Hello, World!
const waits for signal from start
port and then sends a message to print
.
Range expressions work in a similar way
:start -> 1.100 -> ...
Implementation Details
This works thanks to new builtin.NewV2
(name is temporary) native component that behaves almost like builtin.New
except instead just sending configuration message in infinite loop and being only by the speed of receivers, NewV2
has input port sig
- it awaits for input signal and sends a message to receivers, then blocks until new sig
message comes in, and so on and so forth.
Other
- Several minor refactorings and bug-fixes in the compiler, a few more unit-tests added
Pull Requests Included
- feat(readme): hello world, more verbose form for by newcomers by @emil14 in #764
- Update README.md by @emil14 in #765
- feat(docs): add features from v0.26 by @emil14 in #769
- feat(tutorial): basics by @emil14 in #770
- Constants as triggers by @emil14 in #772
Full Changelog: v0.26.0...v0.27.0
v0.26.0
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 functiona...
v0.25.0
It was almost a month after we've released the last version of the language. All this time we've been doing some heavy-lifting. We still need to do it, but bright future is closed day by day. So, what was going on?
Behold, this is the biggest Nevalang release so far!
New Runtime Implementation
In order to fix out-of-order delivery issue with fan-in connections we've completely changed the way runtime operates.
New algorithm properly handles fan-in messages thanks to atomic counter that is used to mark each message when it's sent. With this approach we can serialize messages and deliver them in the same order they was sent. However, out-of-order might still happen in more complex cases. Please see #689 for more details.
New Runtime Functions API
With massive runtime rewrite we've also changed how runtime functions interact with the runtime. New API is much simpler and safe than the old one. You don't have to handle context cancelation manually or iterate over array port slots. No more interactions with the raw go channels. Checkout how runtime functions look now, it's about x1.5 less code!
Fan-Out Connections are now Syntax Sugar
With new design runtime can properly handle fan-in connections but can't handle fan-out. Thankfully this is solved by removing fan-out from low-level program representation completely. Fan-out connections now only exist as syntax sugar in high level representation. Compiler desugarer transforms fan-out connections to regular pipelines with new FanOut
component in standard library. This component has the following signature:
pub flow FanOut(data any) ([data] any)
Graph Reduction Optimization (WIP)
Implemented algorithm that can turn IR (low-level program representation) graph into much smaller but functionally equal one. It is disabled now and needs a little bit more testing. We will enable it by default or under feature flag in next releases.
Performance Boost
Last, but not least - with new runtime Nevalang v0.25 is faster than v0.24 by approximately two orders of magnitude. I didn't do any benchmarks but examples/99_bottles
runs x200 times faster that before... And this is without graph reduction enabled!
Crazy thing about this is that this improvement is accidental! Our goal was to me it correct, not fast. However, performance improvement can be seen by naked eye.
Another funny thing is that now, when runtime works so fast, it's possible to spot new issues that cannot occur when program works slowly. New issues are described in #689.