Compare commits

...

3 Commits

Author SHA1 Message Date
Marc Vertes
3fbebb3662 fix: avoid memory leak in closure
Simplify frame management. Remove node dependency on frame pointer.

Fixes #1618
2024-04-03 18:22:04 +02:00
Marc Vertes
2c92a7c7ab fix: do not panic when assigning to _ (blank) var.
Fix interp.isEmptyInterface to tolerate a nil type.

Fixes #1619
2024-04-02 19:18:03 +02:00
Ludovic Fernandez
9aa161f2da chore: update CI
* chore: update CI

* chore: update CI
2024-03-06 09:12:03 +01:00
16 changed files with 140 additions and 112 deletions

View File

@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
go-version: [ '1.21', '1.22' ]
go-version: [ oldstable, stable ]
os: [ubuntu-latest, macos-latest, windows-latest]
include:
@@ -29,36 +29,18 @@ jobs:
go-path-suffix: \go
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
stable: true
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: go/src/github.com/traefik/yaegi
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
go-version: ${{ matrix.go-version }}
stable: true
- name: Setup GOPATH
run: go env -w GOPATH=${{ github.workspace }}${{ matrix.go-path-suffix }}

View File

@@ -7,8 +7,8 @@ on:
pull_request:
env:
GO_VERSION: '1.22'
GOLANGCI_LINT_VERSION: v1.55.2
GO_VERSION: stable
GOLANGCI_LINT_VERSION: v1.56.2
jobs:
@@ -16,16 +16,16 @@ jobs:
name: Linting
runs-on: ubuntu-latest
steps:
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Check and get dependencies
run: |
go mod tidy
@@ -45,19 +45,19 @@ jobs:
needs: linting
strategy:
matrix:
go-version: [ '1.21', '1.22' ]
go-version: [ oldstable, stable ]
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
stable: true
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Check generated code
run: |
rm -f interp/op.go
@@ -76,21 +76,21 @@ jobs:
working-directory: ${{ github.workspace }}/go/src/github.com/traefik/yaegi
strategy:
matrix:
go-version: [ '1.21', '1.22' ]
go-version: [ oldstable, stable ]
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
stable: true
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: go/src/github.com/traefik/yaegi
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
stable: true
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3

View File

@@ -6,7 +6,7 @@ on:
- v[0-9]+.[0-9]+*
env:
GO_VERSION: '1.21'
GO_VERSION: stable
jobs:
@@ -15,28 +15,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Go modules
uses: actions/cache@v3
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
go-version: ${{ env.GO_VERSION }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --rm-dist
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_REPO }}

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.*.swo
.*.swp
*.dot
*.out
.idea/
/yaegi
internal/cmd/extract/extract

View File

@@ -142,9 +142,10 @@ linters:
issues:
exclude-use-default: false
max-per-linter: 0
max-issues-per-linter: 0
max-same-issues: 0
exclude: []
exclude:
- 'fmt.Sprintf can be replaced with string'
exclude-rules:
- path: .+_test\.go
linters:

View File

@@ -47,7 +47,7 @@ archives:
- LICENSE
brews:
- tap:
- repository:
owner: traefik
name: homebrew-tap
commit_author:

51
_test/issue-1618.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
"fmt"
"runtime"
"sync"
)
func humanizeBytes(bytes uint64) string {
const (
_ = iota
kB uint64 = 1 << (10 * iota)
mB
gB
tB
pB
)
switch {
case bytes < kB:
return fmt.Sprintf("%dB", bytes)
case bytes < mB:
return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kB))
case bytes < gB:
return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mB))
case bytes < tB:
return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gB))
case bytes < pB:
return fmt.Sprintf("%.2fTB", float64(bytes)/float64(tB))
default:
return fmt.Sprintf("%dB", bytes)
}
}
func main() {
i := 0
wg := sync.WaitGroup{}
for {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("#%d: alloc = %s, routines = %d, gc = %d\n", i, humanizeBytes(m.Alloc), runtime.NumGoroutine(), m.NumGC)
wg.Add(1)
go func() {
wg.Done()
}()
wg.Wait()
i = i + 1
}
}

9
_test/op10.go Normal file
View File

@@ -0,0 +1,9 @@
package main
func main() {
_ = 1 + 1
println("ok")
}
// Output:
// ok

10
_test/op11.go Normal file
View File

@@ -0,0 +1,10 @@
package main
func main() {
a, b := 1, 2
_ = a + b
println("ok")
}
// Output:
// ok

View File

@@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
@@ -38,7 +39,7 @@ func extractCmd(arg []string) error {
args := eflag.Args()
if len(args) == 0 {
return fmt.Errorf("missing package")
return errors.New("missing package")
}
license, err := genLicense(licensePath)

View File

@@ -387,7 +387,7 @@ func (e *Extractor) importPath(pkgIdent, importPath string) (string, error) {
return "", err
}
if err != nil {
if len(pkgIdent) > 0 && pkgIdent[0] == '.' {
if pkgIdent != "" && pkgIdent[0] == '.' {
// pkgIdent is definitely a relative path, not a package name, and it does not exist
return "", err
}

View File

@@ -272,10 +272,6 @@ func (dbg *Debugger) enterCall(nFunc, nCall *node, f *frame) {
switch nFunc.kind {
case funcLit:
f.debug.kind = frameCall
if nFunc.frame != nil {
nFunc.frame.debug.kind = frameClosure
nFunc.frame.debug.node = nFunc
}
case funcDecl:
f.debug.kind = frameCall

View File

@@ -33,7 +33,6 @@ type node struct {
tnext *node // true branch successor (CFG)
fnext *node // false branch successor (CFG)
interp *Interpreter // interpreter context
frame *frame // frame pointer used for closures only (TODO: suppress this)
index int64 // node index (dot display)
findex int // index of value in frame or frame size (func def, type def)
level int // number of frame indirections to access value
@@ -138,7 +137,7 @@ func newFrame(anc *frame, length int, id uint64) *frame {
func (f *frame) runid() uint64 { return atomic.LoadUint64(&f.id) }
func (f *frame) setrunid(id uint64) { atomic.StoreUint64(&f.id, id) }
func (f *frame) clone(fork bool) *frame {
func (f *frame) clone() *frame {
f.mutex.RLock()
defer f.mutex.RUnlock()
nf := &frame{
@@ -150,12 +149,8 @@ func (f *frame) clone(fork bool) *frame {
done: f.done,
debug: f.debug,
}
if fork {
nf.data = make([]reflect.Value, len(f.data))
copy(nf.data, f.data)
} else {
nf.data = f.data
}
nf.data = make([]reflect.Value, len(f.data))
copy(nf.data, f.data)
return nf
}

View File

@@ -80,6 +80,7 @@ func TestInterpConsistencyBuild(t *testing.T) {
file.Name() == "time0.go" || // display time (similar to random number)
file.Name() == "factor.go" || // bench
file.Name() == "fib.go" || // bench
file.Name() == "issue-1618.go" || // bench (infinite running)
file.Name() == "type5.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types
file.Name() == "type6.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types

View File

@@ -988,9 +988,6 @@ func genFunctionWrapper(n *node) func(*frame) reflect.Value {
funcType := n.typ.TypeOf()
return func(f *frame) reflect.Value {
if n.frame != nil { // Use closure context if defined.
f = n.frame
}
return reflect.MakeFunc(funcType, func(in []reflect.Value) []reflect.Value {
// Allocate and init local frame. All values to be settable and addressable.
fr := newFrame(f, len(def.types), f.runid())
@@ -1292,14 +1289,13 @@ func call(n *node) {
}
n.exec = func(f *frame) bltn {
var def *node
var ok bool
f.mutex.Lock()
bf := value(f)
if def, ok = bf.Interface().(*node); ok {
def, ok := bf.Interface().(*node)
if ok {
bf = def.rval
}
f.mutex.Unlock()
// Call bin func if defined
if bf.IsValid() {
@@ -1343,12 +1339,7 @@ func call(n *node) {
return tnext
}
anc := f
// Get closure frame context (if any)
if def.frame != nil {
anc = def.frame
}
nf := newFrame(anc, len(def.types), anc.runid())
nf := newFrame(f, len(def.types), f.runid())
var vararg reflect.Value
// Init return values
@@ -1887,27 +1878,22 @@ func getIndexMap2(n *node) {
}
}
const fork = true // Duplicate frame in frame.clone().
// getFunc compiles a closure function generator for anonymous functions.
func getFunc(n *node) {
i := n.findex
l := n.level
next := getExec(n.tnext)
numRet := len(n.typ.ret)
n.exec = func(f *frame) bltn {
fr := f.clone(fork)
nod := *n
nod.val = &nod
nod.frame = fr
def := &nod
numRet := len(def.typ.ret)
fr := f.clone()
o := getFrame(f, l).data[i]
fct := reflect.MakeFunc(nod.typ.TypeOf(), func(in []reflect.Value) []reflect.Value {
fct := reflect.MakeFunc(n.typ.TypeOf(), func(in []reflect.Value) []reflect.Value {
// Allocate and init local frame. All values to be settable and addressable.
fr2 := newFrame(fr, len(def.types), fr.runid())
fr2 := newFrame(fr, len(n.types), fr.runid())
d := fr2.data
for i, t := range def.types {
for i, t := range n.types {
d[i] = reflect.New(t).Elem()
}
d = d[numRet:]
@@ -1918,7 +1904,7 @@ func getFunc(n *node) {
// In case of unused arg, there may be not even a frame entry allocated, just skip.
break
}
typ := def.typ.arg[i]
typ := n.typ.arg[i]
switch {
case isEmptyInterface(typ) || typ.TypeOf() == valueInterfaceType:
d[i].Set(arg)
@@ -1930,12 +1916,19 @@ func getFunc(n *node) {
}
// Interpreter code execution.
runCfg(def.child[3].start, fr2, def, n)
runCfg(n.child[3].start, fr2, n, n)
f.mutex.Lock()
getFrame(f, l).data[i] = o
f.mutex.Unlock()
return fr2.data[:numRet]
})
f.mutex.Lock()
getFrame(f, l).data[i] = fct
f.mutex.Unlock()
return next
}
}
@@ -1946,11 +1939,9 @@ func getMethod(n *node) {
next := getExec(n.tnext)
n.exec = func(f *frame) bltn {
fr := f.clone(!fork)
nod := *(n.val.(*node))
nod.val = &nod
nod.recv = n.recv
nod.frame = fr
getFrame(f, l).data[i] = genFuncValue(&nod)(f)
return next
}
@@ -2021,11 +2012,9 @@ func getMethodByName(n *node) {
panic(n.cfgErrorf("method not found: %s", name))
}
fr := f.clone(!fork)
nod := *m
nod.val = &nod
nod.recv = &receiver{nil, val.value, li}
nod.frame = fr
getFrame(f, l).data[i] = genFuncValue(&nod)(f)
return next
}

View File

@@ -2393,7 +2393,7 @@ func isMap(t *itype) bool { return t.TypeOf().Kind() == reflect.Map }
func isPtr(t *itype) bool { return t.TypeOf().Kind() == reflect.Ptr }
func isEmptyInterface(t *itype) bool {
return t.cat == interfaceT && len(t.field) == 0
return t != nil && t.cat == interfaceT && len(t.field) == 0
}
func isGeneric(t *itype) bool {