Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ bench:
# https://code.visualstudio.com/api/working-with-extensions/publishing-extension#packaging-extensions
.PHONY: install-tools
install-tools:
npm install -g vsce
npm install -g vsce typescript

.PHONY: extension-login
extension-login:
Expand Down
32 changes: 32 additions & 0 deletions ast/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,3 +596,35 @@ func (r *Range) String() string {
out.WriteString(r.container.String())
return out.String()
}

// Receive is an expression node that describes receiving from a channel
type Receive struct {
// the "<-" token
token token.Token

// the channel to receive from
channel Node
}

// NewReceive creates a new Receive node.
func NewReceive(token token.Token, channel Node) *Receive {
return &Receive{token: token, channel: channel}
}

func (r *Receive) ExpressionNode() {}

func (r *Receive) IsExpression() bool { return true }

func (r *Receive) Token() token.Token { return r.token }

func (r *Receive) Literal() string { return r.token.Literal }

func (r *Receive) Channel() Node { return r.channel }

func (r *Receive) String() string {
var out bytes.Buffer
out.WriteString(r.Literal())
out.WriteString(" ")
out.WriteString(r.channel.String())
return out.String()
}
122 changes: 116 additions & 6 deletions ast/statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ast

import (
"bytes"
"fmt"
"strings"

"github.com/risor-io/risor/token"
Expand Down Expand Up @@ -146,9 +147,9 @@ func (c *Const) String() string {
return out.String()
}

// Control defines a return, break, or continue statement.
// Branch defines a break or continue statement.
type Control struct {
// "return", "break", or "continue"
// "break", or "continue"
token token.Token

// optional value, for return statements
Expand Down Expand Up @@ -183,6 +184,39 @@ func (c *Control) String() string {
return out.String()
}

// Return defines a return statement.
type Return struct {
// "return"
token token.Token

// optional value
value Expression
}

// NewReturn creates a new Return node.
func NewReturn(token token.Token, value Expression) *Return {
return &Return{token: token, value: value}
}

func (r *Return) StatementNode() {}

func (r *Return) IsExpression() bool { return false }

func (r *Return) Token() token.Token { return r.token }

func (r *Return) Literal() string { return r.token.Literal }

func (r *Return) Value() Expression { return r.value }

func (r *Return) String() string {
var out bytes.Buffer
out.WriteString(r.Literal())
if r.value != nil {
out.WriteString(" " + r.value.Literal())
}
return out.String()
}

// Block is a node that holds a sequence of statements. This is used to
// represent the body of a function, loop, or a conditional.
type Block struct {
Expand Down Expand Up @@ -211,10 +245,8 @@ func (b *Block) EndsWithReturn() bool {
return false
}
last := b.statements[count-1]
if cntrl, ok := last.(*Control); ok {
return cntrl.IsReturn()
}
return false
_, isReturn := last.(*Return)
return isReturn
}

func (b *Block) String() string {
Expand Down Expand Up @@ -519,3 +551,81 @@ func (e *SetAttr) String() string {
out.WriteString(e.value.String())
return out.String()
}

// A Go statement node represents a go statement.
type Go struct {
token token.Token
call Expression
}

Comment on lines +555 to +560
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be very useful if the go keyword was an expression instead of a statement.

And have it return the Risor thread object.

Because then you could have:

x := go thing()

x.wait()

In other words, it would be semantically identical to:

x := spawn(thing)

x.wait()

but with nicer syntax. Because then using Risor to do some quick parallel things becomes super easy. Such as:

a := go fetch("https://url-a")
b := go fetch("https://url-b")
c := go fetch("https://url-c")

my_handling_func(a.wait(), b.wait(), c.wait())

And at that point, you don't really need to expose the spawn builtin function, but instead only rely on the go keyword.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely considered this. I do think there's something to be said for matching Go's behavior for the go keyword exactly though. It seems like a plus if most or all go statements in Risor are also valid in Go. There's an element of making it the least surprising to a Go programmer. This is why I chose to keep it as a statement for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue the other way around. That Go syntax and semantics could be expected to work in Risor, but then Risor could add stuff on top to make scripting easier.

Changing from a statement to an expression only expands its usage. So Go semantics are still valid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

risor is not go and if it provides some functionality that is similar but not exactly the same it only makes things more confusing, I'd even argue it would be better to stick to spawn and thread and not use the go keyword at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just find the spawn syntax so inconvenient. Similar to Risor's try. Switching between function calling syntax is confusing and I commonly trip on it while writing Risor code.

You're fully right @luisdavim that reusing the go keyword does bring over a lot of assumptions from the programmer.

Maybe a different keyword could be used, like just using the word spawn

spawn(my_func, 1, 2)
spawn my_func(1, 2)

Or what if spawn was a builtin property on all functions?

my_func(1, 2) // run immediately
my_func.spawn(1, 2) // spawn a goroutine

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added support for this:

my_func.spawn(1, 2) // spawn a goroutine

I did keep the go keyword as a statement, so it doesn't return the thread handle. If you want the thread handle you should use spawn.

This decision keeps the go behavior very similar to Go's for now, and doesn't preclude us from switching it to an expression down the road.

// NewGo creates a new Go statement node.
func NewGo(token token.Token, call Expression) *Go {
return &Go{token: token, call: call}
}

func (g *Go) StatementNode() {}

func (g *Go) IsExpression() bool { return false }

func (g *Go) Token() token.Token { return g.token }

func (g *Go) Literal() string { return g.token.Literal }

func (g *Go) Call() Expression { return g.call }

func (g *Go) String() string {
return fmt.Sprintf("go %s", g.call.String())
}

// A Defer statement node represents a defer statement.
type Defer struct {
token token.Token
call Expression
}

// NewDefer creates a new Defer node.
func NewDefer(token token.Token, call Expression) *Defer {
return &Defer{token: token, call: call}
}

func (d *Defer) StatementNode() {}

func (d *Defer) IsExpression() bool { return false }

func (d *Defer) Token() token.Token { return d.token }

func (d *Defer) Literal() string { return d.token.Literal }

func (d *Defer) Call() Expression { return d.call }

func (d *Defer) String() string {
return fmt.Sprintf("defer %s", d.call.String())
}

// A Send statement node represents a channel send operation.
type Send struct {
token token.Token
channel Expression
value Expression
}

// NewSend creates a new Send node.
func NewSend(token token.Token, channel, value Expression) *Send {
return &Send{token: token, channel: channel, value: value}
}

func (s *Send) StatementNode() {}

func (s *Send) IsExpression() bool { return false }

func (s *Send) Token() token.Token { return s.token }

func (s *Send) Literal() string { return s.token.Literal }

func (s *Send) Channel() Expression { return s.channel }

func (s *Send) Value() Expression { return s.value }

func (s *Send) String() string {
return fmt.Sprintf("%s <- %s", s.channel.String(), s.value.String())
}
Loading