fix ssa2ast bugs - #1080
Merged
Merged
Conversation
Convert failed with "instruction multiconvert ... : unsupported" for
generic conversions whose type set spans multiple conversion forms, such
as converting a type parameter to a concrete type:
func genericConvert[T ~int64 | ~float64](x T) float64 {
return float64(x)
}
MultiConvert is a representation-changing conversion, so emit the same
(T)(x) cast as Convert.
The recvOk tuple slot of a select receive was declared but never assigned,
so "case v, ok := <-ch" always observed ok == false (the zero value) even
when a value was received from an open channel:
select {
case v, ok := <-ch:
fmt.Println(v, ok) // ok was always false
Emit the receive with comma-ok (v, ok = <-ch) when the ok slot has uses.
Ranging over a named map or string type panicked:
panic: interface conversion: types.Type is *types.Named, not *types.Map
type namedMap map[string]int
type namedString string
for k := range namedMap{...} {...} // panicked
for _, r := range namedString("x") {...} // panicked
isStringType now checks the underlying type, and the map iterator polyfill
is built from the underlying *types.Map instead of asserting directly.
The string range polyfill iterated []rune(x) and reported the rune index
as the range key, but a string range yields byte offsets. For non-ASCII
strings the two diverge:
for i, r := range "héllo" {
fmt.Println(i, r) // got 0,1,2,3,4; want 0,1,3,4,5
}
Track the byte offset separately and advance it by len(string(r)) per rune
while the rune slice supplies the values.
Converting a bound method value failed with:
make closure for non anon func "Return1$bound": unsupported
x/tools lowers `f := strct.Return1` (a MethodVal) to a MakeClosure over a
synthetic `$bound` wrapper whose only free variable is the receiver, so
the converter must not treat it as an anonymous function.
Reconstruct the method value as `recv.method`; for instantiated methods
the wrapper name carries type args (`get[int]$bound`) which are stripped
since the method value infers them from the receiver type.
A named type declared inside a nested block (if/for/switch) produced
"undefined: local" in the converted output. The previous check only
inlined function-scoped types, using a reflect hack on the unexported
Scope.isFunc field; a block scope is not a function scope, so such types
were left as dangling identifier references.
func f() {
if true {
type local struct{ X int }
_ = local{1} // converted to: undefined: local
}
}
Inline any named type whose declaration scope is not its package scope,
and drop the reflect dependency.
A composite literal of a struct with a blank field ("_ int") produced
"undefined: x._" in the converted output, because the field address was
reconstructed as &x._ — but blank fields cannot be referenced in Go.
type S struct {
_ int
X int
}
s := S{5, 7} // converted to: &s._ (invalid)
Emit new(T) for the blank field's address instead; the subsequent store
becomes a no-op that still evaluates its value expression, preserving any
side effects.
The byte-index polyfill advanced the offset by len(string(r)), the UTF-8
width of the decoded rune. For an invalid byte the range yields U+FFFD,
whose own encoding is 3 bytes wide, but the range advances only 1 byte:
for i, r := range "\xff\x41" {
fmt.Println(i, r) // got 0,3; want 0,1
}
Build the rune slice and its byte offsets together with a native range
over the string (which decodes invalid sequences correctly) instead of
deriving offsets from []rune(x) and each rune's own width.
Inlining a generic type declared in a block scope used the declaration's
underlying type, which still references the generic type parameter, so the
converted output referenced an undefined T:
func f() {
type pair[T any] struct{ a, b T }
p := pair[int]{1, 2} // converted to: struct{ a T; b T } (undefined: T)
}
Inline the instantiated type's underlying type (typ.Underlying()) instead
of the declaration's (obj.Type().Underlying()), so the type arguments are
substituted before conversion.
Field access on a struct referenced through a type alias failed:
type Local = struct{ X int }
x := Local{1}
fmt.Println(x.X) // field 0 not found in test/main.Local
getFieldName unwrapped pointers and named types but not aliases, so a
*types.Alias never reached the struct case. Resolve aliases with
types.Unalias before unwrapping the named type.
mvdan
approved these changes
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix a batch of bugs in the ssa2ast converter used by the control flow obfuscation, so it correctly handles more valid Go constructs
p.s. partly found via LLM fuzzing