diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fc4e1d3e..50ed9360 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,10 +17,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.60 + version: v1.64 only-new-issues: true diff --git a/.gitignore b/.gitignore index 3ea0f1a2..088be565 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ cmd/tmp /neva-lsp-windows-arm64.exe dist trace.log +ir.yml output node_modules __debug* diff --git a/.vscode/launch.json b/.vscode/launch.json index 94b8cc29..9c6f6f08 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cmd/neva", - "cwd": "${workspaceFolder}/e2e/for_with_range_and_if", + "cwd": "${workspaceFolder}/e2e/errors_must", "args": ["run", "--trace", "main"] }, { diff --git a/benchmarks/message_passing/bench_test.go b/benchmarks/message_passing/bench_test.go index 33e99fff..3032f693 100644 --- a/benchmarks/message_passing/bench_test.go +++ b/benchmarks/message_passing/bench_test.go @@ -30,7 +30,7 @@ func BenchmarkMessagePassing(b *testing.B) { // Reset timer after setup b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { cmd := exec.Command("neva", "run", "message_passing") out, err := cmd.CombinedOutput() require.NoError(b, err, string(out)) diff --git a/e2e/cli/run_with_ir/e2e_test.go b/e2e/cli/run_with_ir/e2e_test.go new file mode 100644 index 00000000..d60f56ff --- /dev/null +++ b/e2e/cli/run_with_ir/e2e_test.go @@ -0,0 +1,39 @@ +package test + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func Test(t *testing.T) { + defer func() { + require.NoError(t, os.RemoveAll("src")) + require.NoError(t, os.Remove("ir.yml")) + }() + + // Create new project + cmd := exec.Command("neva", "new") + require.NoError(t, cmd.Run()) + + // Run with IR emission + cmd = exec.Command("neva", "run", "--emit-ir", "src") + out, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Equal(t, "Hello, World!\n", string(out)) + require.Equal(t, 0, cmd.ProcessState.ExitCode()) + + // Verify IR file exists and is valid YAML + irBytes, err := os.ReadFile("ir.yml") + require.NoError(t, err) + + var ir struct { + Connections map[string]string `yaml:"connections"` + Funcs []any `yaml:"funcs"` + } + require.NoError(t, yaml.Unmarshal(irBytes, &ir)) + require.NotEmpty(t, ir.Funcs) +} diff --git a/e2e/cli/run_with_ir/neva.yml b/e2e/cli/run_with_ir/neva.yml new file mode 100644 index 00000000..5d36b277 --- /dev/null +++ b/e2e/cli/run_with_ir/neva.yml @@ -0,0 +1 @@ +neva: 0.31.0 \ No newline at end of file diff --git a/e2e/errors_must/e2e_test.go b/e2e/errors_must/e2e_test.go index b3906b99..56bcd624 100644 --- a/e2e/errors_must/e2e_test.go +++ b/e2e/errors_must/e2e_test.go @@ -8,15 +8,18 @@ import ( ) func Test(t *testing.T) { - cmd := exec.Command("neva", "run", "main") + // we run N times to make sure https://github.com/nevalang/neva/issues/872 is fixed + for range 10 { + cmd := exec.Command("neva", "run", "main") - out, err := cmd.CombinedOutput() - require.NoError(t, err) - require.Equal( - t, - "success!\n", - string(out), - ) + out, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Equal( + t, + "success!\n", + string(out), + ) - require.Equal(t, 0, cmd.ProcessState.ExitCode()) + require.Equal(t, 0, cmd.ProcessState.ExitCode()) + } } diff --git a/examples/delayed_echo/e2e_test.go b/examples/delayed_echo/e2e_test.go index 9f8080c8..d4606c12 100644 --- a/examples/delayed_echo/e2e_test.go +++ b/examples/delayed_echo/e2e_test.go @@ -11,6 +11,7 @@ import ( ) func Test(t *testing.T) { + // for i := 0; i < 10; i++ { err := os.Chdir("..") require.NoError(t, err) @@ -23,7 +24,7 @@ func Test(t *testing.T) { start := time.Now() out, err := cmd.CombinedOutput() elapsed := time.Since(start) - require.NoError(t, err) + require.NoError(t, err, string(out)) // Check execution time is between 1-5 seconds require.GreaterOrEqual(t, elapsed.Seconds(), 1.0) @@ -31,10 +32,10 @@ func Test(t *testing.T) { // Split output into lines and verify contents lines := strings.Split(strings.TrimSpace(string(out)), "\n") - require.Equal(t, 7, len(lines)) // Hello + World + 5 numbers + require.Equal(t, 7, len(lines), string(out)) // Hello + World + 5 numbers // First line must be Hello - require.Equal(t, "Hello", lines[0]) + require.Equal(t, "Hello", lines[0], string(out)) // Create set of expected remaining values expected := map[string]bool{ @@ -59,4 +60,5 @@ func Test(t *testing.T) { } require.Equal(t, 0, cmd.ProcessState.ExitCode()) + // } } diff --git a/go.mod b/go.mod index fbe2457b..e7d39d3c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nevalang/neva -go 1.23 +go 1.24 require ( github.com/Masterminds/semver/v3 v3.2.1 diff --git a/internal/cli/run.go b/internal/cli/run.go index 11081b4b..9042c4bf 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -22,6 +22,10 @@ func newRunCmd(workdir string, nativec compiler.Compiler) *cli.Command { Name: "trace", Usage: "Write trace information to file", }, + &cli.BoolFlag{ + Name: "emit-ir", + Usage: "Emit intermediate representation to ir.yml file", + }, }, ArgsUsage: "Provide path to main package", Action: func(cliCtx *cli.Context) error { @@ -30,10 +34,8 @@ func newRunCmd(workdir string, nativec compiler.Compiler) *cli.Command { return err } - var trace bool - if cliCtx.IsSet("trace") { - trace = true - } + trace := cliCtx.IsSet("trace") + emitIR := cliCtx.IsSet("emit-ir") // we need to always set GOOS for compiler backend prevGOOS := os.Getenv("GOOS") @@ -55,6 +57,7 @@ func newRunCmd(workdir string, nativec compiler.Compiler) *cli.Command { Main: mainPkg, Output: workdir, Trace: trace, + EmitIR: emitIR, } if err := nativec.Compile(cliCtx.Context, input); err != nil { diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 569ea699..c864a31a 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -3,8 +3,13 @@ package compiler import ( "context" "errors" + "fmt" + "os" + "path/filepath" "strings" + "gopkg.in/yaml.v3" + "github.com/nevalang/neva/internal/compiler/ir" "github.com/nevalang/neva/internal/compiler/sourcecode" "github.com/nevalang/neva/internal/compiler/sourcecode/core" @@ -20,6 +25,7 @@ type CompilerInput struct { Main string Output string Trace bool + EmitIR bool } func (c Compiler) Compile(ctx context.Context, input CompilerInput) error { @@ -33,9 +39,26 @@ func (c Compiler) Compile(ctx context.Context, input CompilerInput) error { return err } + if input.EmitIR { + if err := c.emitIR(input.Output, meResult.IR); err != nil { + return fmt.Errorf("emit IR: %w", err) + } + } + return c.be.Emit(input.Output, meResult.IR, input.Trace) } +func (c Compiler) emitIR(dst string, prog *ir.Program) error { + path := filepath.Join(dst, "ir.yml") + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + // fmt.Println(dst, path, prog) + return yaml.NewEncoder(f).Encode(prog) +} + type Frontend struct { builder Builder parser Parser diff --git a/internal/compiler/desugarer/desugarer.go b/internal/compiler/desugarer/desugarer.go index 2bf7d02a..e5d9aa03 100644 --- a/internal/compiler/desugarer/desugarer.go +++ b/internal/compiler/desugarer/desugarer.go @@ -120,8 +120,7 @@ type Scope interface { Entity(ref core.EntityRef) (src.Entity, core.Location, error) Relocate(location core.Location) src.Scope Location() *core.Location - GetFirstInportName(nodes map[string]src.Node, portAddr src.PortAddr) (string, error) - GetFirstOutportName(nodes map[string]src.Node, portAddr src.PortAddr) (string, error) + GetNodeIOByPortAddr(nodes map[string]src.Node, portAddr src.PortAddr) (src.IO, error) } func (d *Desugarer) desugarPkg(pkg src.Package, scope Scope) (src.Package, error) { diff --git a/internal/compiler/desugarer/mocks_test.go b/internal/compiler/desugarer/mocks_test.go index 58116e5c..25b4b082 100644 --- a/internal/compiler/desugarer/mocks_test.go +++ b/internal/compiler/desugarer/mocks_test.go @@ -51,34 +51,19 @@ func (mr *MockScopeMockRecorder) Entity(ref interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Entity", reflect.TypeOf((*MockScope)(nil).Entity), ref) } -// GetFirstInportName mocks base method. -func (m *MockScope) GetFirstInportName(nodes map[string]sourcecode.Node, portAddr sourcecode.PortAddr) (string, error) { +// GetNodeIOByPortAddr mocks base method. +func (m *MockScope) GetNodeIOByPortAddr(nodes map[string]sourcecode.Node, portAddr sourcecode.PortAddr) (sourcecode.IO, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFirstInportName", nodes, portAddr) - ret0, _ := ret[0].(string) + ret := m.ctrl.Call(m, "GetNodeIOByPortAddr", nodes, portAddr) + ret0, _ := ret[0].(sourcecode.IO) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetFirstInportName indicates an expected call of GetFirstInportName. -func (mr *MockScopeMockRecorder) GetFirstInportName(nodes, portAddr interface{}) *gomock.Call { +// GetNodeIOByPortAddr indicates an expected call of GetNodeIOByPortAddr. +func (mr *MockScopeMockRecorder) GetNodeIOByPortAddr(nodes, portAddr interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstInportName", reflect.TypeOf((*MockScope)(nil).GetFirstInportName), nodes, portAddr) -} - -// GetFirstOutportName mocks base method. -func (m *MockScope) GetFirstOutportName(nodes map[string]sourcecode.Node, portAddr sourcecode.PortAddr) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFirstOutportName", nodes, portAddr) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFirstOutportName indicates an expected call of GetFirstOutportName. -func (mr *MockScopeMockRecorder) GetFirstOutportName(nodes, portAddr interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstOutportName", reflect.TypeOf((*MockScope)(nil).GetFirstOutportName), nodes, portAddr) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeIOByPortAddr", reflect.TypeOf((*MockScope)(nil).GetNodeIOByPortAddr), nodes, portAddr) } // Location mocks base method. diff --git a/internal/compiler/desugarer/network.go b/internal/compiler/desugarer/network.go index 936a348d..e928314d 100644 --- a/internal/compiler/desugarer/network.go +++ b/internal/compiler/desugarer/network.go @@ -1,6 +1,7 @@ package desugarer import ( + "errors" "fmt" "github.com/nevalang/neva/internal/compiler" @@ -238,7 +239,7 @@ func (d *Desugarer) desugarSingleReceiver( }, nil } - firstInportName, err := scope.GetFirstInportName(nodes, *receiver.PortAddr) + firstInportName, err := d.getFirstInportName(scope, nodes, *receiver.PortAddr) if err != nil { return desugarReceiverResult{}, fmt.Errorf("get first inport name: %w", err) } @@ -441,7 +442,7 @@ func (d *Desugarer) desugarChainedConnection( chainHeadPort = chainHead.PortAddr.Port if chainHeadPort == "" { var err error - chainHeadPort, err = scope.GetFirstInportName(nodes, *chainHead.PortAddr) + chainHeadPort, err = d.getFirstInportName(scope, nodes, *chainHead.PortAddr) if err != nil { return desugarConnectionResult{}, fmt.Errorf("get first inport name: %w", err) } @@ -688,7 +689,7 @@ func (d *Desugarer) desugarSingleSender( if sender.PortAddr != nil { portName := sender.PortAddr.Port if sender.PortAddr.Port == "" { - firstOutportName, err := scope.GetFirstOutportName(nodes, *sender.PortAddr) + firstOutportName, err := d.getFirstOutportName(scope, nodes, *sender.PortAddr) if err != nil { return desugarSenderResult{}, fmt.Errorf("get first outport name: %w", err) } @@ -842,6 +843,42 @@ func (d *Desugarer) desugarSingleSender( }, nil } +func (d *Desugarer) getFirstInportName( + scope Scope, + nodes map[string]src.Node, + portAddr src.PortAddr, +) (string, error) { + io, err := scope.GetNodeIOByPortAddr(nodes, portAddr) + if err != nil { + return "", err + } + for inport := range io.In { + return inport, nil + } + return "", errors.New("first inport not found") +} + +func (d *Desugarer) getFirstOutportName( + scope Scope, + nodes map[string]src.Node, + portAddr src.PortAddr, +) (string, error) { + io, err := scope.GetNodeIOByPortAddr(nodes, portAddr) + if err != nil { + return "", err + } + + // important: skip `err` outport if node has err guard + for outport := range io.Out { + if outport == "err" && nodes[portAddr.Node].ErrGuard { + continue + } + return outport, nil + } + + return "", errors.New("first outport not found") +} + var newComponentRef = core.EntityRef{ Pkg: "builtin", Name: "New", diff --git a/internal/compiler/ir/ir.go b/internal/compiler/ir/ir.go index d7b77744..0160fc28 100644 --- a/internal/compiler/ir/ir.go +++ b/internal/compiler/ir/ir.go @@ -1,19 +1,22 @@ package ir -import "fmt" +import ( + "encoding/json" + "fmt" +) // Program is a graph where ports are vertexes and connections are edges. type Program struct { - Connections map[PortAddr]PortAddr `json:"connections,omitempty"` - Funcs []FuncCall `json:"funcs,omitempty"` + Connections map[PortAddr]PortAddr `json:"connections,omitempty" yaml:"connections,omitempty"` + Funcs []FuncCall `json:"funcs,omitempty" yaml:"funcs,omitempty"` } // PortAddr is a composite unique identifier for a port. type PortAddr struct { - Path string `json:"path,omitempty"` // List of upstream nodes including the owner of the port. - Port string `json:"port,omitempty"` // Name of the port. - Idx uint8 `json:"idx,omitempty"` // Optional index of a slot in array port. - IsArray bool `json:"isArray,omitempty"` // Flag to indicate that the port is an array. + Path string `json:"path,omitempty" yaml:"path,omitempty"` // List of upstream nodes including the owner of the port. + Port string `json:"port,omitempty" yaml:"port,omitempty"` // Name of the port. + Idx uint8 `json:"idx,omitempty" yaml:"idx,omitempty"` // Optional index of a slot in array port. + IsArray bool `json:"isArray,omitempty" yaml:"isArray,omitempty"` // Flag to indicate that the port is an array. } func (p PortAddr) String() string { @@ -23,28 +26,36 @@ func (p PortAddr) String() string { return fmt.Sprintf("%s:%s[%d]", p.Path, p.Port, p.Idx) } +func (p PortAddr) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} + +func (p PortAddr) MarshalYAML() (any, error) { + return p.String(), nil +} + // FuncCall describes call of a runtime function. type FuncCall struct { - Ref string `json:"ref,omitempty"` // Reference to the function in registry. - IO FuncIO `json:"io,omitempty"` // Input/output ports of the function. - Msg *Message `json:"msg,omitempty"` // Optional initialization message. + Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` // Reference to the function in registry. + IO FuncIO `json:"io" yaml:"io"` // Input/output ports of the function. + Msg *Message `json:"msg,omitempty" yaml:"msg,omitempty"` // Optional initialization message. } // FuncIO is how a runtime function gets access to its ports. type FuncIO struct { - In []PortAddr `json:"in,omitempty"` // Must be ordered by path -> port -> idx. - Out []PortAddr `json:"out,omitempty"` // Must be ordered by path -> port -> idx. + In []PortAddr `json:"in,omitempty" yaml:"in,omitempty"` // Must be ordered by path -> port -> idx. + Out []PortAddr `json:"out,omitempty" yaml:"out,omitempty"` // Must be ordered by path -> port -> idx. } // Message is a data that can be sent and received. type Message struct { - Type MsgType `json:"-"` - Bool bool `json:"bool,omitempty"` - Int int64 `json:"int,omitempty"` - Float float64 `json:"float,omitempty"` - String string `json:"str,omitempty"` - List []Message `json:"list,omitempty"` - DictOrStruct map[string]Message `json:"map,omitempty"` + Type MsgType `json:"type" yaml:"type"` + Bool bool `json:"bool,omitempty" yaml:"bool,omitempty"` + Int int64 `json:"int,omitempty" yaml:"int,omitempty"` + Float float64 `json:"float,omitempty" yaml:"float,omitempty"` + String string `json:"str,omitempty" yaml:"str,omitempty"` + List []Message `json:"list,omitempty" yaml:"list,omitempty"` + DictOrStruct map[string]Message `json:"map,omitempty" yaml:"map,omitempty"` } // MsgType is an enumeration of message types. diff --git a/internal/compiler/parser/smoke_test/mocks_test.go b/internal/compiler/parser/smoke_test/mocks_test.go index 10578049..2203c6c0 100644 --- a/internal/compiler/parser/smoke_test/mocks_test.go +++ b/internal/compiler/parser/smoke_test/mocks_test.go @@ -1,7 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. // Source: smoke_test.go -// Package frontend_test is a generated GoMock package. +// Package smoke_test is a generated GoMock package. package smoke_test import ( diff --git a/internal/compiler/sourcecode/scope.go b/internal/compiler/sourcecode/scope.go index 2227d0b1..1167333a 100644 --- a/internal/compiler/sourcecode/scope.go +++ b/internal/compiler/sourcecode/scope.go @@ -167,9 +167,9 @@ func (s Scope) entity(entityRef core.EntityRef) (Entity, core.Location, error) { }, nil } -func (s Scope) getNodeIOByPortAddr( +func (s Scope) GetNodeIOByPortAddr( nodes map[string]Node, - portAddr *PortAddr, + portAddr PortAddr, ) (IO, error) { node, ok := nodes[portAddr.Node] if !ok { @@ -191,17 +191,6 @@ func (s Scope) getNodeIOByPortAddr( return iface.IO, nil } -func (s Scope) GetFirstInportName(nodes map[string]Node, portAddr PortAddr) (string, error) { - io, err := s.getNodeIOByPortAddr(nodes, &portAddr) - if err != nil { - return "", err - } - for inport := range io.In { - return inport, nil - } - return "", errors.New("first inport not found") -} - func (s Scope) GetEntityKind(entityRef core.EntityRef) (EntityKind, error) { entity, _, err := s.entity(entityRef) if err != nil { @@ -209,14 +198,3 @@ func (s Scope) GetEntityKind(entityRef core.EntityRef) (EntityKind, error) { } return entity.Kind, nil } - -func (s Scope) GetFirstOutportName(nodes map[string]Node, portAddr PortAddr) (string, error) { - io, err := s.getNodeIOByPortAddr(nodes, &portAddr) - if err != nil { - return "", err - } - for outport := range io.Out { - return outport, nil - } - return "", errors.New("first outport not found") -}