diff --git a/internal/ssa2ast/func.go b/internal/ssa2ast/func.go index 2b1ee45d..38031da1 100644 --- a/internal/ssa2ast/func.go +++ b/internal/ssa2ast/func.go @@ -139,6 +139,7 @@ func isVoidType(typ types.Type) bool { } func isStringType(typ types.Type) bool { + typ = typ.Underlying() return types.Identical(typ, types.Typ[types.String]) || types.Identical(typ, types.Typ[types.UntypedString]) } @@ -146,6 +147,7 @@ func getFieldName(tp types.Type, index int) (string, error) { if pt, ok := tp.(*types.Pointer); ok { tp = pt.Elem() } + tp = types.Unalias(tp) if named, ok := tp.(*types.Named); ok { tp = named.Underlying() } @@ -564,6 +566,12 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock return err } stmt = defineVar(instr, castExpr) + case *ssa.MultiConvert: + castExpr, err := fc.castCallExpr(instr.Type(), instr.X) + if err != nil { + return err + } + stmt = defineVar(instr, castExpr) case *ssa.Defer: callExpr, err := fc.convertCall(instr.Call) if err != nil { @@ -594,6 +602,19 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock if err != nil { return err } + + if fieldName == "_" { + // Blank fields cannot be referenced; the store to them is a + // no-op, but its value expression may still have side effects. + fieldType := instr.Type().Underlying().(*types.Pointer).Elem() + fieldTypeExpr, err := fc.tc.Convert(fieldType) + if err != nil { + return err + } + stmt = defineVar(instr, ah.CallExprByName("new", fieldTypeExpr)) + break + } + stmt = defineVar(instr, &ast.UnaryExpr{ Op: token.AND, X: ah.SelectExpr(xExpr, ast.NewIdent(fieldName)), @@ -732,12 +753,13 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock } if instr.IsString { - idxName := fc.tupleVarName(instr.Iter, 0) + runeIdxName := fc.tupleVarName(instr.Iter, 0) iterValName := fc.tupleVarName(instr.Iter, 1) + offsetsName := fc.tupleVarName(instr.Iter, 2) stmt = ah.BlockStmt( ah.AssignStmt(ast.NewIdent(okName), &ast.BinaryExpr{ - X: ast.NewIdent(idxName), + X: ast.NewIdent(runeIdxName), Op: token.LSS, Y: ah.CallExprByName("len", ast.NewIdent(iterValName)), }), @@ -747,9 +769,9 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent(keyName), ast.NewIdent(valName)}, Tok: token.ASSIGN, - Rhs: []ast.Expr{ast.NewIdent(idxName), ah.IndexExprByExpr(ast.NewIdent(iterValName), ast.NewIdent(idxName))}, + Rhs: []ast.Expr{ah.IndexExprByExpr(ast.NewIdent(offsetsName), ast.NewIdent(runeIdxName)), ah.IndexExprByExpr(ast.NewIdent(iterValName), ast.NewIdent(runeIdxName))}, }, - &ast.IncDecStmt{X: ast.NewIdent(idxName), Tok: token.INC}, + &ast.IncDecStmt{X: ast.NewIdent(runeIdxName), Tok: token.INC}, ), }, ) @@ -779,22 +801,32 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock return err } if isStringType(instr.X.Type()) { - idxName := fc.tupleVarName(instr, 0) - valName := fc.tupleVarName(instr, 1) + runeIdxName := fc.tupleVarName(instr, 0) + iterValName := fc.tupleVarName(instr, 1) + offsetsName := fc.tupleVarName(instr, 2) - astFunc.Vars[idxName] = types.Typ[types.Int] - astFunc.Vars[valName] = types.NewSlice(types.Typ[types.Rune]) + astFunc.Vars[runeIdxName] = types.Typ[types.Int] + astFunc.Vars[iterValName] = types.NewSlice(types.Typ[types.Rune]) + astFunc.Vars[offsetsName] = types.NewSlice(types.Typ[types.Int]) - stmt = &ast.AssignStmt{ - Lhs: []ast.Expr{ast.NewIdent(idxName), ast.NewIdent(valName)}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{ - ah.IntLit(0), - ah.CallExpr(&ast.ArrayType{Elt: ast.NewIdent("rune")}, xExpr), + idxName := fc.tupleVarName(instr, 3) + runeName := fc.tupleVarName(instr, 4) + + stmt = ah.BlockStmt( + &ast.RangeStmt{ + Key: ast.NewIdent(idxName), + Value: ast.NewIdent(runeName), + Tok: token.DEFINE, + X: xExpr, + Body: ah.BlockStmt( + ah.AssignStmt(ast.NewIdent(iterValName), ah.CallExprByName("append", ast.NewIdent(iterValName), ast.NewIdent(runeName))), + ah.AssignStmt(ast.NewIdent(offsetsName), ah.CallExprByName("append", ast.NewIdent(offsetsName), ast.NewIdent(idxName))), + ), }, - } + ah.AssignStmt(ast.NewIdent(runeIdxName), ah.IntLit(0)), + ) } else { - makeIterExpr, nextType, err := makeMapIteratorPolyfill(fc.tc, instr.X.Type().(*types.Map)) + makeIterExpr, nextType, err := makeMapIteratorPolyfill(fc.tc, instr.X.Type().Underlying().(*types.Map)) if err != nil { return err } @@ -835,7 +867,16 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock if valHasRefs { astFunc.Vars[valName] = valType } - commStmt = ah.AssignStmt(ast.NewIdent(valName), &ast.UnaryExpr{Op: token.ARROW, X: chanExpr}) + recvExpr := &ast.UnaryExpr{Op: token.ARROW, X: chanExpr} + if okHasRefs { + commStmt = &ast.AssignStmt{ + Lhs: []ast.Expr{ast.NewIdent(valName), ast.NewIdent(okName)}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{recvExpr}, + } + } else { + commStmt = ah.AssignStmt(ast.NewIdent(valName), recvExpr) + } recvIndex++ default: return fmt.Errorf("not supported select chan dir %d: %w", state.Dir, ErrUnsupported) @@ -975,6 +1016,17 @@ func (fc *funcConverter) convertBlock(astFunc *AstFunc, ssaBlock *ssa.BasicBlock } case *ssa.MakeClosure: anonFunc := instr.Fn.(*ssa.Function) + if strings.HasSuffix(anonFunc.Name(), "$bound") { + recvExpr, err := fc.convertSsaValue(instr.Bindings[0]) + if err != nil { + return err + } + methodName := strings.TrimSuffix(anonFunc.Name(), "$bound") + methodName, _, _ = strings.Cut(methodName, "[") + stmt = defineVar(instr, ah.SelectExpr(recvExpr, ast.NewIdent(methodName))) + break + } + anonFuncName, err := fc.getAnonFunctionName(anonFunc) if err != nil { return err diff --git a/internal/ssa2ast/func_test.go b/internal/ssa2ast/func_test.go index be5521e0..c1b32742 100644 --- a/internal/ssa2ast/func_test.go +++ b/internal/ssa2ast/func_test.go @@ -136,6 +136,10 @@ func slicesOps() { return } +type namedMap map[string]int + +type namedString string + func iterAndMapsOps() { sprintf := makeSprintf("iterAndMapsOps") @@ -163,7 +167,28 @@ func iterAndMapsOps() { sprintf(idx, s) } + for i, r := range "héllo" { + sprintf(i, r) + } + + for i, r := range "\xff\x41" { + sprintf(i, r) + } + sprintf(mmap["April"].String()) + + nm := namedMap{"a": 1, "b": 2} + var nmkeys []string + for k := range nm { + nmkeys = append(nmkeys, k) + } + sort.Strings(nmkeys) + sprintf(nmkeys) + + ns := namedString("hello") + for i, r := range ns { + sprintf(i, r) + } return } @@ -234,6 +259,12 @@ func methodOps() { thunkMethod2 := (*structCalls).Return2 sprintf(thunkMethod2(&strct)) + boundMethod := strct.Return1 + sprintf(boundMethod()) + + boundMethod2 := strct.Return2 + sprintf(boundMethod2()) + closureVar := "c " + s anonFnc := func(n func(structCalls) string) string { return n(structCalls{}) + "anon" + closureVar @@ -272,6 +303,22 @@ func chanOps() { val, ok := <-a sprintf(val, ok) + + f := make(chan string, 1) + f <- "x" + select { + case r, ok := <-f: + sprintf("sel-ok", r, ok) + default: + sprintf("sel-default") + } + close(f) + select { + case r, ok := <-f: + sprintf("sel-closed", r, ok) + default: + sprintf("sel-default2") + } return } @@ -305,6 +352,13 @@ type testStruct struct { A, B int } +type blankFieldStruct struct { + _ int + X int +} + +type aliasStruct = struct{ X int } + func typeOps() { sprintf := makeSprintf("typeOps") @@ -331,6 +385,12 @@ func typeOps() { strc.B += strc.A sprintf(strc) + bs := blankFieldStruct{return42(), 7} + sprintf(bs.X) + + as := aliasStruct{return42()} + sprintf(as.X) + // Access to unexported structure discard := io.Discard if return42() == 0 { @@ -347,6 +407,14 @@ func sumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { return s } +type genericBox[T any] struct { + val T +} + +func (b genericBox[T]) get() T { + return b.val +} + func genericFunc() { sprintf := makeSprintf("genericFunc") @@ -361,6 +429,17 @@ func genericFunc() { "second": 12.1, } sprintf(sumIntsOrFloats(floats)) + + sprintf(genericConvert(int64(34))) + sprintf(genericConvert(float64(12.5))) + + box := genericBox[int]{val: 42} + getVal := box.get + sprintf(getVal()) +} + +func genericConvert[T ~int64 | ~float64](x T) float64 { + return float64(x) } ` diff --git a/internal/ssa2ast/type.go b/internal/ssa2ast/type.go index 017899b0..1a75c74b 100644 --- a/internal/ssa2ast/type.go +++ b/internal/ssa2ast/type.go @@ -5,7 +5,6 @@ import ( "go/ast" "go/token" "go/types" - "reflect" "strconv" ) @@ -99,12 +98,10 @@ func (tc *TypeConverter) Convert(typ types.Type) (ast.Expr, error) { case *types.Named: obj := typ.Obj() - // TODO: rewrite struct inlining without reflection hack - if parent := obj.Parent(); parent != nil { - isFuncScope := reflect.ValueOf(parent).Elem().FieldByName("isFunc") - if isFuncScope.Bool() { - return tc.Convert(obj.Type().Underlying()) - } + // A named type declared in a local (function or block) scope is not + // part of the converted output, so reference its underlying type. + if obj.Pkg() != nil && obj.Parent() != obj.Pkg().Scope() { + return tc.Convert(typ.Underlying()) } var namedExpr ast.Expr diff --git a/internal/ssa2ast/type_test.go b/internal/ssa2ast/type_test.go index 2bfae8a7..d49e5b1c 100644 --- a/internal/ssa2ast/type_test.go +++ b/internal/ssa2ast/type_test.go @@ -2,6 +2,7 @@ package ssa2ast import ( "go/ast" + "go/types" "testing" "github.com/go-quicktest/qt" @@ -102,3 +103,74 @@ func TestTypeToExpr(t *testing.T) { structConvAst := convAst.(*ast.StructType) qt.Assert(t, qt.CmpEquals(structConvAst, structAst, astCmpOpt)) } + +func TestConvertLocalType(t *testing.T) { + _, _, info, _ := mustParseAndTypeCheckFile(`package main + +func f() { + if true { + type local struct{ X int } + var _ local + } +} +`) + + var localType *types.Named + for _, obj := range info.Defs { + if tn, ok := obj.(*types.TypeName); ok && tn.Name() == "local" { + localType = tn.Type().(*types.Named) + } + } + if localType == nil { + t.Fatal("local type not found") + } + + fc := &TypeConverter{resolver: defaultImportNameResolver} + convAst, err := fc.Convert(localType) + qt.Assert(t, qt.IsNil(err)) + + // Local (function/block-scoped) types are inlined to their underlying + // type, since the name is not emitted in the converted output. + structAst, ok := convAst.(*ast.StructType) + if !ok { + t.Fatalf("Convert(local type) = %T, want *ast.StructType (inlined)", convAst) + } + qt.Assert(t, qt.Equals(len(structAst.Fields.List), 1)) + qt.Assert(t, qt.Equals(structAst.Fields.List[0].Names[0].Name, "X")) +} + +func TestConvertLocalGenericType(t *testing.T) { + _, _, info, _ := mustParseAndTypeCheckFile(`package main + +func f() { + type pair[T any] struct{ a, b T } + var p pair[int] + _ = p +} +`) + + var pairType *types.Named + for _, obj := range info.Defs { + if v, ok := obj.(*types.Var); ok && v.Name() == "p" { + pairType = v.Type().(*types.Named) + } + } + if pairType == nil { + t.Fatal("pair type not found") + } + + fc := &TypeConverter{resolver: defaultImportNameResolver} + convAst, err := fc.Convert(pairType) + qt.Assert(t, qt.IsNil(err)) + + // The instantiated type's type arguments must be substituted into the + // inlined underlying type; the generic type parameter T is out of scope. + structAst, ok := convAst.(*ast.StructType) + if !ok { + t.Fatalf("Convert(pair[int]) = %T, want *ast.StructType (inlined)", convAst) + } + qt.Assert(t, qt.Equals(len(structAst.Fields.List), 2)) + for _, f := range structAst.Fields.List { + qt.Assert(t, qt.Equals(f.Type.(*ast.Ident).Name, "int")) + } +}