nevalang / neva

🌊 Dataflow programming language with static types and implicit parallelism. Compiles to native code and Go
https://nevalang.org
MIT License
91 stars 7 forks source link

Any program that uses the same sender (port) twice or more is incorrect #473

Open emil14 opened 7 months ago

emil14 commented 7 months ago

Because runtime implements sort-of pub-sub algorithm based on spawning a goroutine per connection where connection is one sender -> multiple receivers.

If we have e.g. 2 connections with the same foo:bar receiver, then we got 2 goroutines each receiving from foo:bar. So we miss message in one of these two goroutines.

We have 2 ways to fix this, easy and hard (I like the easy one, it's good for language API)

  1. Hard - merge all connections with the same sender into one big connections (could not be easy or even possible due to existing of then connections, can we mix them with normal ones? isn't it a problem in general (not related to this issue))
  2. Easy - force compiler (analyzer) to always have only one connection with specific sender

Might be related to https://github.com/nevalang/neva/issues/627

ajzaff commented 6 months ago

Hi, Does this issue imply something like 2x is hard?

E.g.

2 -> x
x -> adder[0]
x -> adder[1]
addr[res] -> out
emil14 commented 6 months ago

@ajzaff hey there

Not really, this is about more complex cases. Your example can be covered with single connection like this:

someNode:x -> [
  adder:data[0],
  adder:data[1]
]
adder:res -> :someOutport

In case x is constant then it's $x -> ... and rest is the same.

emil14 commented 6 months ago

So here's real example, sub-component from 99 bottles example:

const {
    firstLineTpl string = '$0 bottles of beer on the wall, $0 bottles of beer.'
    secondLineTpl1 string = 'Take one down and pass it around, $0 bottles of beer on the wall.'
    secondLine2 string = 'Take one down and pass it around, no more bottles of beer on the wall.'
}

component Looper(old int) (new int) {
    nodes {
        Eq<int>
        Printer<any>
        Decrementor<int>
        fprinter1 FPrinter<int>
        fprinter2 FPrinter<int>
    }
    net {
        // print first line
        :old -> [
            fprinter1:args[0],
            ($firstLineTpl -> fprinter1:tpl)
        ]

        // decrement from old
        fprinter1:args[0] -> decrementor:data

        // if decremented == 0
        decrementor:res -> [
            eq:a,
            (0 -> eq:b)
        ]

        // then print second line and end send new to out
        eq:then -> ($secondLine2 -> printer:data)
        printer:sig -> (eq:then -> :new)

        // else print another second line
        eq:else.a -> [
            fprinter2:args[0],
            ($secondLineTpl1 -> fprinter2:tpl)
        ]

        // and send new to out
        fprinter2:args[0] -> :new
    }
}

These lines:

eq:then -> ($secondLine2 -> printer:data)
printer:sig -> (eq:then -> :new)

Causes problem.

  1. eq:then sends a message (in this current example is 0 int)
  2. Runtime's connector reads that message in goroutine that handles first connection and sends it to virtual blocker (it triggers deferred connection)
  3. printer:sig sends message (some string), we wanna use that signal to send message from eq:then to :new outport but the message from Eq is already sent and lost.

This specific example can be solved by using explicitly 0 instead of reusing eq:then but there are probably cases where this is impossible.

Possible solution would be iterating over all connections of a desugared program (it's important, desugarer inserts virtual connections) and grouping them by sender.

emil14 commented 6 months ago

Workaround

We can explicitly use Blocker component instead of deferred connections