Skip to content

Commit

Permalink
Merge pull request #591 from nevalang/err_guard
Browse files Browse the repository at this point in the history
Error guard `?` operator
  • Loading branch information
emil14 committed May 4, 2024
2 parents 1c76784 + 4831a30 commit cc76527
Show file tree
Hide file tree
Showing 32 changed files with 1,986 additions and 1,584 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"mode": "auto",
"program": "${workspaceFolder}/cmd/neva",
"cwd": "${workspaceFolder}",
"args": ["run", "examples/hello_world"]
"args": ["run", "e2e/advanced_error_handling/main"]
},
{
"name": "LSP",
Expand Down
30 changes: 30 additions & 0 deletions examples/advanced_error_handling/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package test

import (
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/require"
)

func Test(t *testing.T) {
err := os.Chdir("..")
require.NoError(t, err)

wd, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(wd)

cmd := exec.Command("neva", "run", "advanced_error_handling")
out, err := cmd.CombinedOutput()
require.NoError(t, err)
require.Equal(
t,
`panic: {"text": "Get \"definitely%20not%20a%20valid%20URL\": unsupported protocol scheme \"\""}
`,
string(out),
)

require.Equal(t, 0, cmd.ProcessState.ExitCode())
}
18 changes: 18 additions & 0 deletions examples/advanced_error_handling/main.neva
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { http }

component Main(start) (stop) {
nodes { App, Println, Panic } // Panic will crash the program
net {
:start -> app:sig
app:err -> panic // we only handle err at this lvl
app:data -> println -> :stop
}
}

component App(sig) (data string, err error) {
nodes { http.Get? } // '?' implicitly sends err downstream
net {
:sig -> ('definitely not a valid URL' -> get)
get:resp.body -> :data // look ma, no error handling!
}
}
14 changes: 0 additions & 14 deletions examples/http/get/main.neva

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (
)

func Test(t *testing.T) {
err := os.Chdir("../..")
err := os.Chdir("..")
require.NoError(t, err)

wd, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(wd)

cmd := exec.Command("neva", "run", "http/get")
cmd := exec.Command("neva", "run", "http_get")

out, err := cmd.CombinedOutput()
require.NoError(t, err)
Expand Down
9 changes: 9 additions & 0 deletions examples/http_get/main.neva
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { http }

component Main(start) (stop) {
nodes { http.Get, Println }
net {
:start -> ('http://www.example.com' -> get)
get:resp.body -> println -> :stop
}
}
5 changes: 3 additions & 2 deletions internal/compiler/analyzer/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ func (a Analyzer) analyzeComponent( //nolint:funlen
return component, nil
}

resolvedNodes, nodesIfaces, err := a.analyzeComponentNodes(
component.Interface.TypeParams,
resolvedNodes, nodesIfaces, hasGuard, err := a.analyzeComponentNodes(
component.Interface,
component.Nodes,
scope,
)
Expand All @@ -112,6 +112,7 @@ func (a Analyzer) analyzeComponent( //nolint:funlen
analyzedNet, err := a.analyzeComponentNetwork(
component.Net,
resolvedInterface,
hasGuard,
resolvedNodes,
nodesIfaces,
scope,
Expand Down
75 changes: 58 additions & 17 deletions internal/compiler/analyzer/component_net.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ import (
)

var (
ErrUnusedOutports = errors.New("All component's outports are unused")
ErrUnusedOutport = errors.New("Unused outport found")
ErrUnusedInports = errors.New("All component inports are unused")
ErrUnusedInport = errors.New("Unused inport found")
ErrLiteralSenderTypeEmpty = errors.New("Literal network sender must contain message value")
ErrComplexLiteralSender = errors.New("Literal network sender must have primitive type")
ErrIllegalPortlessConnection = errors.New("Connection to a node, with more than one port, must always has a port name")
ErrUnusedOutports = errors.New("All component's outports are unused")
ErrUnusedOutport = errors.New("Unused outport found")
ErrUnusedInports = errors.New("All component inports are unused")
ErrUnusedInport = errors.New("Unused inport found")
ErrLiteralSenderTypeEmpty = errors.New("Literal network sender must contain message value")
ErrComplexLiteralSender = errors.New("Literal network sender must have primitive type")
ErrIllegalPortlessConnection = errors.New("Connection to a node, with more than one port, must always has a port name")
ErrGuardMixedWithExplicitErrConn = errors.New("If node has error guard '?' it's ':err' outport must not be explicitly used in the network")
)

// analyzeComponentNetwork must be called after analyzeNodes so we sure nodes are resolved.
func (a Analyzer) analyzeComponentNetwork(
net []src.Connection,
compInterface src.Interface,
hasGuard bool,
nodes map[string]src.Node,
nodesIfaces map[string]foundInterface,
scope src.Scope,
Expand All @@ -36,7 +38,14 @@ func (a Analyzer) analyzeComponentNetwork(
return nil, compiler.Error{Location: &scope.Location}.Wrap(err)
}

if err := a.checkNetPortsUsage(compInterface, nodesIfaces, scope, nodesUsage); err != nil {
if err := a.checkNetPortsUsage(
compInterface,
nodesIfaces,
hasGuard,
scope,
nodesUsage,
nodes,
); err != nil {
return nil, compiler.Error{Location: &scope.Location}.Wrap(err)
}

Expand Down Expand Up @@ -321,8 +330,10 @@ type nodeNetUsage struct {
func (Analyzer) checkNetPortsUsage(
compInterface src.Interface,
nodesIfaces map[string]foundInterface,
hasGuard bool,
scope src.Scope,
nodesUsage map[string]nodeNetUsage,
nodes map[string]src.Node,
) *compiler.Error {
inportsUsage, ok := nodesUsage["in"]
if !ok {
Expand Down Expand Up @@ -352,11 +363,18 @@ func (Analyzer) checkNetPortsUsage(
}

for outportName := range compInterface.IO.Out {
if _, ok := outportsUsage.In[outportName]; !ok { // note that self outports are inports for the network
return &compiler.Error{
Err: fmt.Errorf("%w '%v'", ErrUnusedOutport, outportName),
Location: &scope.Location,
}
// note that self outports are inports for the network
if _, ok := outportsUsage.In[outportName]; ok {
continue
}

if outportName == "err" && hasGuard {
continue
}

return &compiler.Error{
Err: fmt.Errorf("%w '%v'", ErrUnusedOutport, outportName),
Location: &scope.Location,
}
}

Expand All @@ -380,7 +398,10 @@ func (Analyzer) checkNetPortsUsage(

return &compiler.Error{
Err: fmt.Errorf(
"%w: %v:%v", ErrUnusedNodeInport, nodeName, inportName,
"%w: %v:%v",
ErrUnusedNodeInport,
nodeName,
inportName,
),
Location: &scope.Location,
Meta: &meta,
Expand All @@ -394,10 +415,30 @@ func (Analyzer) checkNetPortsUsage(

atLeastOneOutportIsUsed := false
for outportName := range nodeIface.iface.IO.Out {
if _, ok := nodeUsage.Out[outportName]; ok {
atLeastOneOutportIsUsed = true
break
if _, ok := nodeUsage.Out[outportName]; !ok {
continue
}

atLeastOneOutportIsUsed = true

if outportName != "err" {
continue
}

meta := nodes[nodeName].Meta

if nodes[nodeName].ErrGuard {
return &compiler.Error{
Err: fmt.Errorf(
"%w: %v",
ErrGuardMixedWithExplicitErrConn,
nodeName,
),
Location: &scope.Location,
Meta: &meta,
}
}

}

if !atLeastOneOutportIsUsed {
Expand Down
49 changes: 43 additions & 6 deletions internal/compiler/analyzer/component_nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
ErrAutoPortsTypeParamConstr = errors.New("Component that uses struct inports directive must have type parameter with struct constraint")
ErrAutoPortsTypeParamsCount = errors.New("Component that uses struct inports directive must have type parameter with have exactly one type parameter")
ErrNormalInportsWithAutoPortsDirective = errors.New("Component that uses struct inports directive must have no defined inports")
ErrGuardNotAllowedForNode = errors.New("Guard is not allowed for nodes without 'err' output")
ErrGuardNotAllowedForComponent = errors.New("Guard is not allowed for components without 'err' output")
)

type foundInterface struct {
Expand All @@ -25,21 +27,31 @@ type foundInterface struct {
}

func (a Analyzer) analyzeComponentNodes(
parentTypeParams src.TypeParams,
componentIface src.Interface,
nodes map[string]src.Node,
scope src.Scope,
) (
map[string]src.Node, // resolved nodes
map[string]foundInterface, // resolved nodes interfaces with locations
bool, // one of the nodes has error guard
*compiler.Error, // err
) {
analyzedNodes := make(map[string]src.Node, len(nodes))
nodesInterfaces := make(map[string]foundInterface, len(nodes))
hasErrGuard := false

for nodeName, node := range nodes {
analyzedNode, nodeInterface, err := a.analyzeComponentNode(node, parentTypeParams, scope)
if node.ErrGuard {
hasErrGuard = true
}

analyzedNode, nodeInterface, err := a.analyzeComponentNode(
componentIface,
node,
scope,
)
if err != nil {
return nil, nil, compiler.Error{
return nil, nil, false, compiler.Error{
Location: &scope.Location,
Meta: &node.Meta,
}.Wrap(err)
Expand All @@ -49,15 +61,17 @@ func (a Analyzer) analyzeComponentNodes(
analyzedNodes[nodeName] = analyzedNode
}

return analyzedNodes, nodesInterfaces, nil
return analyzedNodes, nodesInterfaces, hasErrGuard, nil
}

//nolint:funlen
func (a Analyzer) analyzeComponentNode(
componentIface src.Interface,
node src.Node,
parentTypeParams src.TypeParams,
scope src.Scope,
) (src.Node, foundInterface, *compiler.Error) {
parentTypeParams := componentIface.TypeParams

nodeEntity, location, err := scope.Entity(node.EntityRef)
if err != nil {
return src.Node{}, foundInterface{}, &compiler.Error{
Expand Down Expand Up @@ -96,6 +110,23 @@ func (a Analyzer) analyzeComponentNode(
return src.Node{}, foundInterface{}, aerr
}

if node.ErrGuard {
if _, ok := componentIface.IO.Out["err"]; !ok {
return src.Node{}, foundInterface{}, &compiler.Error{
Err: ErrGuardNotAllowedForNode,
Location: &scope.Location,
Meta: &node.Meta,
}
}
if _, ok := nodeIface.IO.Out["err"]; !ok {
return src.Node{}, foundInterface{}, &compiler.Error{
Err: ErrGuardNotAllowedForComponent,
Location: &scope.Location,
Meta: &node.Meta,
}
}
}

// We need to get resolved frame from parent type parameters
// in order to be able to resolve node's args
// since they can refer to type parameter of the parent (interface)
Expand Down Expand Up @@ -158,6 +189,7 @@ func (a Analyzer) analyzeComponentNode(
EntityRef: node.EntityRef,
TypeArgs: resolvedNodeArgs,
Meta: node.Meta,
ErrGuard: node.ErrGuard,
}, foundInterface{
iface: nodeIface,
location: location,
Expand All @@ -166,7 +198,11 @@ func (a Analyzer) analyzeComponentNode(

resolvedComponentDI := make(map[string]src.Node, len(node.Deps))
for depName, depNode := range node.Deps {
resolvedDep, _, err := a.analyzeComponentNode(depNode, parentTypeParams, scope)
resolvedDep, _, err := a.analyzeComponentNode(
componentIface,
depNode,
scope,
)
if err != nil {
return src.Node{}, foundInterface{}, compiler.Error{
Location: &location,
Expand All @@ -182,6 +218,7 @@ func (a Analyzer) analyzeComponentNode(
TypeArgs: resolvedNodeArgs,
Deps: resolvedComponentDI,
Meta: node.Meta,
ErrGuard: node.ErrGuard,
}, foundInterface{
iface: nodeIface,
location: location,
Expand Down
Loading

0 comments on commit cc76527

Please sign in to comment.