risor-io / risor

Fast and flexible scripting for Go developers and DevOps.
https://risor.io
Apache License 2.0
581 stars 24 forks source link

Support for make, go, defer, and channels #178

Closed myzie closed 5 months ago

myzie commented 6 months ago

Reproduces various Go style functionality in Risor relating to the go and defer keywords, the chan type, and the make built-in function. The spawn built-in function here is equivalent to spawning a goroutine, but it returns the result of the spawned function when the returned thread's .wait() method is called.

Channels can be created using chan() or chan(capacity) or make(chan, capacity).

The make built-in can be used to create various Risor types with the given capacity, e.g. make(map, 5).

Result is 1:

c := chan(1); go func() { c <- 1 }(); <-c

Result is 34:

func test(x) { return x + 1 }; spawn(test, 33).wait()

Result is [1, 2, 3]:

    l := []
    func foo(value) {
        defer l.append(value+1) // 3
        defer l.append(value)   // 2
        l.append(1)             // 1
    }
    foo(2)
    l
applejag commented 6 months ago

We could make a new version of Clone() that used the same exact globals map instead.

I don't think that's a good idea. That would easily lead to race conditions, and but instead to design away any race conditions (at the cost of a little performance).

This proposal you have here is interesting, but it only protects against race conditions on scalars, but does not protect objects. Maybe you could do deep copies on all globals too, but what if x was a file handle?

For example, this breaks:

x := {
    y: 0,
}

func doit() {
    for i := 0; i < 1000; i++ {
        x.y = x.y + 1
    }
}

for range [1,2,3,4] {
    spawn(doit)
}

time.sleep(1)

print(x)
$ go run ./cmd/risor example.risor
fatal error: concurrent map writes

goroutine 20 [running]:
github.com/risor-io/risor/object.(*Map).Set(...)
    /home/kallefagerberg/code/github.com/jillejr/risor/object/map.go:287
github.com/risor-io/risor/object.(*Map).SetAttr(0xc000410d80?, {0xc000408a00?, 0xc000612400?}, {0x12f42f8?, 0xc000612420})
    /home/kallefagerberg/code/github.com/jillejr/risor/object/map.go:56 +0x45
github.com/risor-io/risor/vm.(*VirtualMachine).eval(0xc00011a000, {0x12ef278?, 0xc00043ce70?})
    /home/kallefagerberg/code/github.com/jillejr/risor/vm/vm.go:263 +0x20d8
github.com/risor-io/risor/vm.(*VirtualMachine).callFunction(0xc00011a000, {0x12ef278, 0xc00043ce70}, 0xc00039ea20, {0xc0004c0030, 0x0, 0x0?})
//...snip...

Maybe there could be a bunch of mutexes or channels to interact with global objects, but is it possible to completely seal this? I could imagine there are plenty of ways to break that. To prevent edge cases, it would require all globals to be wrapped, as well as all function arguments and return values, as well as any objects returned by the builtin modules can never refer to shared state.

Also, in the discussion, you mentioned messaging. How would you see messaging be implemented here? Such as just exposing a way to create chan object.Object channels?

myzie commented 6 months ago

Thank you both for taking a peek. I'll be incorporating a number of those things you all mentioned. It was definitely just a starting point.

myzie commented 6 months ago

Generally I don't think there's a clear path to a totally new approach to thread safety, so I'm not aiming for anything too exotic. I think having a message passing and/or channels mechanism, and maybe a mutex type, and then just making a nice API on those is probably enough for our purposes.

myzie commented 5 months ago

@applejag @luisdavim - will you review this?

vm_test.go https://github.com/risor-io/risor/pull/178/files#diff-11f0bc114469cd5519c577c6688384edf8786c307023e008682ffa72dacd8e07

is probably the best bet to see some examples at the moment.

applejag commented 5 months ago

:tada: