Skip to content

Commit

Permalink
update to go1.21 (#1598)
Browse files Browse the repository at this point in the history
* feat: generate go1.21 files

* chore: update CI

* feat: add support for generic symbols in standard library packages

This is necessary to fully support go1.21 and beyond, which now
provide some generic packages such as `cmp`, `maps` or `slices`
in the standard library.

The principle is to embed the generic symbols in source form (as
strings) so they can be instantiated as required during interpretation.

Extract() has been modified to skip the generic types, functions and
constraint interfaces which can't be represented as reflect.Values.

A new stdlib/generic package has been added to provide the corresponding
source files as embedded strings.

The `Use()` function has been changed to pre-parse generic symbols as
doing lazy parsing was causing cyclic dependencies issues at compiling.
This is something we may improve in the future.

A unit test using `cmp` has been added.

For now, there are still some issues with generic stdlib packages
inter-dependencies, for example `slices` importing `cmp`, or when
generic types or function signatures depends on pre-compiled types
in the same package, which we will support shortly.

* fixup

* fixup

* fixup

* fixup

* fixup

* fixup

* fixes for go1.20

* fix previous

* update unsafe2 for go1.21, skip faky tests

In go1.21, the reflect rtype definition has been move to internal/abi.
We follow this change for maintainability, even if there is no layout
change (the go1.20 unsafe2 is compatible with go1.21).

We have isolated a few problematic tests which are failing sometimes
in go1.21, but work in go1.20, and also in go1.22. Those tests are
skipped if in go1.21. A preliminary investigation can not confirm that
something is wrong in yaegi, and the problem disappears with go1.22.

* add new wrapper for go1.21 package testing/slogtest

* add missing wrapper for go/doc/comment

* add support for slices generic package

---------

Co-authored-by: Marc Vertes <mvertes@free.fr>
  • Loading branch information
ldez and mvertes authored Mar 4, 2024
1 parent da27c4f commit 1990b96
Show file tree
Hide file tree
Showing 540 changed files with 7,711 additions and 4,114 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go-cross.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
go-version: [ 1.19, '1.20' ]
go-version: [ '1.20', '1.21' ]
os: [ubuntu-latest, macos-latest, windows-latest]

include:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
pull_request:

env:
GO_VERSION: '1.20'
GOLANGCI_LINT_VERSION: v1.53.3
GO_VERSION: '1.21'
GOLANGCI_LINT_VERSION: v1.55.2

jobs:

Expand Down Expand Up @@ -45,7 +45,7 @@ jobs:
needs: linting
strategy:
matrix:
go-version: [ 1.19, '1.20' ]
go-version: [ '1.20', '1.21' ]
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
working-directory: ${{ github.workspace }}/go/src/github.com/traefik/yaegi
strategy:
matrix:
go-version: [ 1.19, '1.20' ]
go-version: [ '1.20', '1.21' ]

steps:
- name: Set up Go ${{ matrix.go-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- v[0-9]+.[0-9]+*

env:
GO_VERSION: '1.20'
GO_VERSION: '1.21'

jobs:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ It powers executable Go scripts and plugins, in embedded interpreters or interac
* Works everywhere Go works
* All Go & runtime resources accessible from script (with control)
* Security: `unsafe` and `syscall` packages neither used nor exported by default
* Support the latest 2 major releases of Go (Go 1.19 and Go 1.20)
* Support the latest 2 major releases of Go (Go 1.20 and Go 1.21)

## Install

Expand Down
14 changes: 12 additions & 2 deletions extract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import (
"{{$key}}"
{{- end}}
{{- end}}
{{- if or .Val .Typ }}
"{{.ImportPath}}"
{{- end}}
"reflect"
)
Expand Down Expand Up @@ -199,6 +201,10 @@ func (e *Extractor) genContent(importPath string, p *types.Package) ([]byte, err
val[name] = Val{pname, false}
}
case *types.Func:
// Skip generic functions and methods.
if s := o.Type().(*types.Signature); s.TypeParams().Len() > 0 || s.RecvTypeParams().Len() > 0 {
continue
}
val[name] = Val{pname, false}
case *types.Var:
val[name] = Val{pname, true}
Expand All @@ -207,9 +213,13 @@ func (e *Extractor) genContent(importPath string, p *types.Package) ([]byte, err
if t, ok := o.Type().(*types.Named); ok && t.TypeParams().Len() > 0 {
continue
}

typ[name] = pname
if t, ok := o.Type().Underlying().(*types.Interface); ok {
if t.NumMethods() == 0 && t.NumEmbeddeds() != 0 {
// Skip interfaces used to implement constraints for generics.
delete(typ, name)
continue
}
var methods []Method
for i := 0; i < t.NumMethods(); i++ {
f := t.Method(i)
Expand Down Expand Up @@ -468,7 +478,7 @@ func GetMinor(part string) string {
return minor
}

const defaultMinorVersion = 20
const defaultMinorVersion = 21

func genBuildTags() (string, error) {
version := runtime.Version()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/traefik/yaegi

go 1.19
go 1.20
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !go1.21
// +build !go1.21

// Package unsafe2 provides helpers to generate recursive struct types.
package unsafe2

Expand Down
72 changes: 72 additions & 0 deletions internal/unsafe2/go1_21_unsafe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//go:build go1.21
// +build go1.21

// Package unsafe2 provides helpers to generate recursive struct types.
package unsafe2

import (
"reflect"
"unsafe"
)

type dummy struct{}

// DummyType represents a stand-in for a recursive type.
var DummyType = reflect.TypeOf(dummy{})

// The following type sizes must match their original definition in Go src/internal/abi/type.go.
type abiType struct {
_ uintptr
_ uintptr
_ uint32
_ uint8
_ uint8
_ uint8
_ uint8
_ uintptr
_ uintptr
_ int32
_ int32
}

type abiName struct {
Bytes *byte
}

type abiStructField struct {
Name abiName
Typ *abiType
Offset uintptr
}

type abiStructType struct {
abiType
PkgPath abiName
Fields []abiStructField
}

type emptyInterface struct {
typ *abiType
_ unsafe.Pointer
}

// SetFieldType sets the type of the struct field at the given index, to the given type.
//
// The struct type must have been created at runtime. This is very unsafe.
func SetFieldType(s reflect.Type, idx int, t reflect.Type) {
if s.Kind() != reflect.Struct || idx >= s.NumField() {
return
}

rtyp := unpackType(s)
styp := (*abiStructType)(unsafe.Pointer(rtyp))
f := styp.Fields[idx]
f.Typ = unpackType(t)
styp.Fields[idx] = f
}

func unpackType(t reflect.Type) *abiType {
v := reflect.New(t).Elem().Interface()
eface := *(*emptyInterface)(unsafe.Pointer(&v))
return eface.typ
}
4 changes: 2 additions & 2 deletions interp/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,12 @@ func checkConstraint(it, ct *itype) error {
return nil
}
for _, c := range ct.constraint {
if it.equals(c) {
if it.equals(c) || it.matchDefault(c) {
return nil
}
}
for _, c := range ct.ulconstraint {
if it.underlying().equals(c) {
if it.underlying().equals(c) || it.matchDefault(c) {
return nil
}
}
Expand Down
4 changes: 4 additions & 0 deletions interp/interp_consistent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ func TestInterpConsistencyBuild(t *testing.T) {
file.Name() == "type33.go" { // expect error
continue
}
// Skip some tests which are problematic in go1.21 only.
if go121 && testsToSkipGo121[file.Name()] {
continue
}

file := file
t.Run(file.Name(), func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions interp/interp_eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,10 @@ func main() {
}

func TestImportPathIsKey(t *testing.T) {
// FIXME(marc): support of stdlib generic packages like "cmp", "maps", "slices" has changed
// the scope layout by introducing new source packages when stdlib is used.
// The logic of the following test doesn't apply anymore.
t.Skip("This test needs to be reworked.")
// No need to check the results of Eval, as TestFile already does it.
i := interp.New(interp.Options{GoPath: filepath.FromSlash("../_test/testdata/redeclaration-global7")})
if err := i.Use(stdlib.Symbols); err != nil {
Expand Down
12 changes: 12 additions & 0 deletions interp/interp_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"

Expand All @@ -16,6 +17,12 @@ import (
"github.com/traefik/yaegi/stdlib/unsafe"
)

// The following tests sometimes (not always) crash with go1.21 but not with go1.20 or go1.22.
// The reason of failure is not obvious, maybe due to the runtime itself, and will be investigated separately.
var testsToSkipGo121 = map[string]bool{"cli6.go": true, "cli7.go": true, "issue-1276.go": true, "issue-1330.go": true, "struct11.go": true}

var go121 = strings.HasPrefix(runtime.Version(), "go1.21")

func TestFile(t *testing.T) {
filePath := "../_test/str.go"
runCheck(t, filePath)
Expand All @@ -27,10 +34,15 @@ func TestFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}

for _, file := range files {
if filepath.Ext(file.Name()) != ".go" {
continue
}
// Skip some tests which are problematic in go1.21 only.
if go121 && testsToSkipGo121[file.Name()] {
continue
}
file := file
t.Run(file.Name(), func(t *testing.T) {
runCheck(t, filepath.Join(baseDir, file.Name()))
Expand Down
23 changes: 15 additions & 8 deletions interp/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,16 +989,18 @@ func nodeType2(interp *Interpreter, sc *scope, n *node, seen []*node) (t *itype,
rtype = rtype.Elem()
}
t = valueTOf(rtype, withNode(n), withScope(sc))
} else {
err = n.cfgErrorf("undefined selector %s.%s", lt.path, name)
break
}
// Continue search in source package, as it may exist if package contains generics.
fallthrough
case srcPkgT:
pkg := interp.srcPkg[lt.path]
if s, ok := pkg[name]; ok {
t = s.typ
} else {
err = n.cfgErrorf("undefined selector %s.%s", lt.path, name)
if pkg, ok := interp.srcPkg[lt.path]; ok {
if s, ok := pkg[name]; ok {
t = s.typ
break
}
}
err = n.cfgErrorf("undefined selector %s.%s", lt.path, name)
default:
if m, _ := lt.lookupMethod(name); m != nil {
t, err = nodeType2(interp, sc, m.child[2], seen)
Expand Down Expand Up @@ -1533,7 +1535,7 @@ func (t *itype) ordered() bool {
return isInt(typ) || isFloat(typ) || isString(typ)
}

// Equals returns true if the given type is identical to the receiver one.
// equals returns true if the given type is identical to the receiver one.
func (t *itype) equals(o *itype) bool {
switch ti, oi := isInterface(t), isInterface(o); {
case ti && oi:
Expand All @@ -1547,6 +1549,11 @@ func (t *itype) equals(o *itype) bool {
}
}

// matchDefault returns true if the receiver default type is the same as the given one.
func (t *itype) matchDefault(o *itype) bool {
return t.untyped && t.id() == "untyped "+o.id()
}

// MethodSet defines the set of methods signatures as strings, indexed per method name.
type methodSet map[string]string

Expand Down
9 changes: 9 additions & 0 deletions interp/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"path"
"reflect"

gen "github.com/traefik/yaegi/stdlib/generic"
)

// Symbols returns a map of interpreter exported symbol values for the given
Expand Down Expand Up @@ -139,6 +141,13 @@ func (interp *Interpreter) Use(values Exports) error {
// well known stdlib package path.
if _, ok := values["fmt/fmt"]; ok {
fixStdlib(interp)

// Load stdlib generic source.
for _, s := range gen.Sources {
if _, err := interp.Compile(s); err != nil {
return err
}
}
}
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions stdlib/generic/go1_20_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build go1.20 && !go1.21
// +build go1.20,!go1.21

package generic

var Sources = []string{}
59 changes: 59 additions & 0 deletions stdlib/generic/go1_21_cmp.go.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cmp provides types and functions related to comparing
// ordered values.
package cmp

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
//
// Note that floating-point types may contain NaN ("not-a-number") values.
// An operator such as == or < will always report false when
// comparing a NaN value with any other value, NaN or not.
// See the [Compare] function for a consistent way to compare NaN values.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}

// Less reports whether x is less than y.
// For floating-point types, a NaN is considered less than any non-NaN,
// and -0.0 is not less than (is equal to) 0.0.
func Less[T Ordered](x, y T) bool {
return (isNaN(x) && !isNaN(y)) || x < y
}

// Compare returns
//
// -1 if x is less than y,
// 0 if x equals y,
// +1 if x is greater than y.
//
// For floating-point types, a NaN is considered less than any non-NaN,
// a NaN is considered equal to a NaN, and -0.0 is equal to 0.0.
func Compare[T Ordered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN && yNaN {
return 0
}
if xNaN || x < y {
return -1
}
if yNaN || x > y {
return +1
}
return 0
}

// isNaN reports whether x is a NaN without requiring the math package.
// This will always return false if T is not floating-point.
func isNaN[T Ordered](x T) bool {
return x != x
}
Loading

0 comments on commit 1990b96

Please sign in to comment.