Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/compile: incorrect package initialization order for spec example #22326

Closed
chadwhitacre opened this issue Oct 18, 2017 · 42 comments
Closed
Labels
early-in-cycle A change that should be done early in the 3 month dev cycle. FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done. release-blocker
Milestone

Comments

@chadwhitacre
Copy link

From https://golang.org/ref/spec#Package_initialization:

Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization.

[…]

For example, given the declarations

var (
  a = c + b
  b = f()
  c = f()
  d = 3
)

func f() int {
  d++
  return d
}

the initialization order is d, b, c, a.

If Go initializes b before c, then after initialization I'd expect the value of b to be 4 and c to be 5. However, this test outputs b as 5 and c as 4. Swapping the b and c declarations doesn't change the output, but swapping the order in the addition in the declaration of a does change the output. Does this mean that the initialization order in the example is actually d, c, b, a? And that both LHS and RHS are in scope in the phrase "earliest in declaration order"? Or (more likely) am I missing something about what it means to declare and initialize a variable?

P.S. Location in current master:

the initialization order is <code>d</code>, <code>b</code>, <code>c</code>, <code>a</code>.

@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Oct 18, 2017
@ianlancetaylor ianlancetaylor added this to the Go1.10 milestone Oct 18, 2017
@griesemer
Copy link
Contributor

Thanks for the issue. The spec example is correct but cmd/compile is wrong; and I'm pretty sure we have an issue for it, but I can't find it at the moment. go/types also agrees with the spec, and there is a test case for this exact example here).

The spec says:

...by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization...

At first, a is dependent on c and b, so it must wait. Both b and c depend on d so they also must wait. Once d is initialized, both c and b have no (uninitialized) dependency and thus can be initialized. They must be initialized in declaration order; since b is declared before c, b must be initialized first. Hence you get the order d, b, c, a.

By introducing a little helper function, we can have the code print out the initialization order explicitly: https://play.golang.org/p/yiajBYfoSG . As you can see, the order is clearly wrong.

The reason it is wrong for cmd/compile is that the compiler doesn't sort the "ready to initialize" variables in declaration order for some reason.

@griesemer griesemer added NeedsFix The path to resolution is known, but the work has not been done. and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Oct 18, 2017
@griesemer griesemer changed the title spec: Package initialization example is confusing (maybe even wrong?) cmd/compile: incorrect package initialization order for spec example Oct 18, 2017
@chadwhitacre
Copy link
Author

chadwhitacre commented Oct 18, 2017

for some reason

Hmm ... something related to initfix? 🤔

// initfix computes initialization order for a list l of top-level
// declarations and outputs the corresponding list of statements
// to include in the init() function body.
func initfix(l []*Node) []*Node {
var lout []*Node
initplans = make(map[*Node]*InitPlan)
lno := lineno
initreorder(l, &lout)
lineno = lno
initplans = nil
return lout
}

@griesemer
Copy link
Contributor

Yes, somewhere in that file. Haven't investigated.

@chadwhitacre
Copy link
Author

@griesemer Cool, no worries. Verbose dive log on chadwhitacre#1, will report back here if I find anything. :)

@chadwhitacre
Copy link
Author

@griesemer I've got the test running. It passes, which seems odd. My work plan:

  • Modify TestInitOrderInfo so that it fails for the case in question (and perhaps others as well?).
  • Fix it! :) Seems like initorder.go might be implicated.

@ianlancetaylor
Copy link
Contributor

For the record, gccgo gets this right.

@griesemer
Copy link
Contributor

griesemer commented Oct 18, 2017

@whit537 I'm not sure what you mean with the test passes. If you're referring to go/types's test then of course it passes. go/types does this correctly as I mentioned above. The bug is in the compiler (which doesn't use go/types).

@chadwhitacre
Copy link
Author

@griesemer @ianlancetaylor I've been working on writing a test and thought I'd provide an update. I don't find any existing tests for package initialization ordering in the compiler. If I'm missing them by all means please point them out. I'm writing a new sinit_test.go file that so far looks like this (yes, it's broken):

// Copyright 2017 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 gc

import (
	"cmd/compile/internal/syntax"
	"testing"
)

func TestInitOrder(t *testing.T) {
	tests := []struct {
		src string
	}{
		{`package foo; var (bar = 537)`},
	}
	for i, test := range tests {

		parsed, err := syntax.ParseBytes(nil, []byte(test.src), nil, nil, nil, 0)
		initfix(parsed)

		if test.src != `Greetings, program!` {
			t.Errorf("%d: y u no greet? :(", i)
		}
	}
}

I was hoping to use writing a test or three to comprehend the code so I can find the bug, and I'm trying to find a good place in the call chain at which to test even simple package initialization behavior. The call chain I see is: Mainfninitinitfixinitreorder. What I'm up against is that afaict Main does a lot of processing and mutating of xtop before handing it off to fninit and initfix. I'm looking for a way to short-circuit all of that in a test since initfix seems like the important function here. I have a hunch that noder.go might be the ticket.

That's my current status. I would be happy for feedback if you have it, otherwise I will keep plugging away as I can find the time. :)

@mdempsky
Copy link
Member

mdempsky commented Oct 25, 2017

@whit537 For historical reasons (the compiler used to be written in C, so it couldn't use Go's testing package), the compiler is mostly tested through tests in $GOROOT/test. You can run those tests by cd'ing into $GOROOT/test and running go run run.go to run them all or something like go run run.go -- fixedbugs/bug345.go to run a single one.

It looks like there are some initialization related tests in there (e.g., $GOROOT/test/*init*.go).

@chadwhitacre
Copy link
Author

I've created fixedbugs/issue22326.go with the spec example. It fails as expected with

go run run.go -- fixedbugs/issue22326.go

Now I'm trying to see a panic in src/cmd/compile/internal/gc/main.go show up under this test. I've discovered the comment-based execution recipes for run.go (e.g., // run), but haven't yet identified one of those (buildrun?) to help here ...

@chadwhitacre
Copy link
Author

Figured out to rebuild Go with make.bash before testing with go run run.go, and to use printlns instead of panics so that rebuilding succeeds. Now to debug! :)

Lessee here ... 🤔

@chadwhitacre
Copy link
Author

chadwhitacre commented Oct 31, 2017

Here are:

  • example.go, the spec example without the d (not needed to reproduce the bug),
  • a patch (against 3e887ff) to trace the package initialization AST walk,
  • the stderr of go run example.go using the patch, and
  • a mapping of int (as line number) to Op.

My hunch is that the first traversal of c should only make it as far as InitPending, not the whole way to InitDone, so that on the second traversal it runs to completion. In the second traversals c comes after b as desired, since it's in order of the declarations, as opposed to the order in the addition.

A naive attempt to move the init2 over defn.Right to below the subsequent append resulted in a failure to build. My next step is to understand that init2 call better, and consider what condition might follow InitPending to catch this case.

@mdempsky mdempsky modified the milestones: Go1.10, Go1.11 Nov 29, 2017
@mdempsky
Copy link
Member

This issue was present in Go 1.9, so not a regression nor release critical. Bumping to 1.11.

@chadwhitacre
Copy link
Author

Going over this in person with @DeadHeadRussell ... looks like options are:

  1. Introduce some sort of state/context object to send down through the AST walk to make the information about variable declaration order available when calling init over Rlists.
  2. Traverse back up the tree as needed to discover this information.

@chadwhitacre
Copy link
Author

Not obvious that parent is available on Node so (2) might not be a ready option.

@chadwhitacre
Copy link
Author

(1) is pretty clearly the longer-term solution, but also pretty clearly a significant refactor.

@josharian
Copy link
Contributor

sinit.go could use a substantial rewrite. However, that's pretty non-trivial.

Last time I looked at adding state to some sinit.go methods, I found it pretty hard to work with, because walk and sinit call each other recursively, and it gets gnarly quickly. Maybe your needs will be more easily met.

Also, long term, we'd like to get rid of the current Node AST entirely, so any major refactoring to sinit.go would hopefully move in the direction of simplification and elimination of code, e.g. by shifting it into the SSA backend.

That's not really a precise answer to your implicit question, but I hope it helps a little.

@ianlancetaylor ianlancetaylor modified the milestones: Go1.11, Go1.12 Jun 30, 2018
@chadwhitacre
Copy link
Author

The call graphs in sinit.go are indeed gnarly, though I think this bug can be fixed short of a rewrite.

something related to initfix?

Here's a sketch of data structures and an algorithm to add to initfix:

dcls: [a,b,c,d]

deps:
	a: [c,b]
	b: [d]
	c: [d]
	d: []

depsback:
	d: [c,b]
	c: [a]
	b: [a]
	a: []

initreorder(...)

while dcls
	for n in dcls
		if deps[n]
			continue

		lout.append(n)
		dcls.remove(n)

		for o in depsback[n]
			deps[o].remove(n)

dcls seems easy to get under initreorder (modulo a wrinkle around static vs. dynamic declarations; d = 3 doesn't actually end up in lout). deps / depsback are more complicated (but still doable?) because they require descending into init1, I think to the point(s) where out gets appended.

@chadwhitacre
Copy link
Author

Ftr, my first patch was buggy. I submitted a second patch, which passes the test suite.

@chadwhitacre
Copy link
Author

Bump. Does anybody want https://go-review.googlesource.com/c/go/+/156328/?

@gopherbot
Copy link

Change https://golang.org/cl/169898 mentions this issue: cmd/compile: change sinit.go functions into methods

@gopherbot
Copy link

Change https://golang.org/cl/169899 mentions this issue: cmd/compile: move sinit.go globals into InitSchedule

gopherbot pushed a commit that referenced this issue Mar 28, 2019
This will make it easier for subsequent CLs to track additional state
during package initialization scheduling.

Passes toolstash-check.

Updates #22326.

Change-Id: I528792ad34f41a4be52951531eb7525a94c9f350
Reviewed-on: https://go-review.googlesource.com/c/go/+/169898
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
gopherbot pushed a commit that referenced this issue Mar 28, 2019
Eliminates global state from sinit.go.

Passes toolstash-check.

Updates #22326.

Change-Id: Ie3cb14bff625baa20134d1488962ab02d24f0c15
Reviewed-on: https://go-review.googlesource.com/c/go/+/169899
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
@mdempsky
Copy link
Member

What's the correct initialization order for this program?

package main

var (
	_ = f("_", b)
	a = f("a")
	b = f("b")
)

func f(s string, rest ...int) int { print(s); return 0 }

func main() { println() }

cmd/compile outputs b_a, but gccgo and go/types (according to Info.InitOrder) output ab_. I think cmd/compile is correct here.

Though notably, changing _ = f("_", b) to just _ = b causes cmd/compile to output ab; I think the correct output is still ba.

@griesemer @ianlancetaylor What do you think?

@ianlancetaylor
Copy link
Contributor

I agree that b_a seems to be correct. I'm not sure why gccgo gets it wrong. The code is there to do the right thing.

@gopherbot
Copy link

Change https://golang.org/cl/170062 mentions this issue: cmd/compile: fix package initialization ordering

@griesemer
Copy link
Contributor

Regarding the example here, I believe go/types follows the spec, specifically the following paragraph:

More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization.

According to this (specifically the 2nd sentence), for

var (
	_ = f("_", b)
	a = f("a")
	b = f("b")
)

the next earliest (in decl. order) variable that has no dependencies is a, followed by b. After b is initialized, the next earliest variable with no uninitialized dependencies is _ . To me that suggests that the initialization order should be a, b, _, (with corresponding output ab_).

Analogously, for the 2nd example (changing _ = f("_", b) to just _ = b), the init order should be unchanged and the output should be ab.

Thus I believe both go/types and gccgo produce the correct output in this case. @ianlancetaylor Thoughts?

The first sentence in this section is perhaps misleading and should be clarified. I filed #31292 for now.

As an aside, whether a variable is named _ or not doesn't matter. The spec should probably be more precise here as well because it talks about "declaration order" in this section while elsewhere it says that _ identifiers are not declared.

@mdempsky
Copy link
Member

mdempsky commented Apr 5, 2019

After chatting with @griesemer about this, I've changed my mind and agree that ab_ does seem to be the right behavior prescribed by the "ready for initialization" paragraph (and that gccgo and go/types are correct).

@ianlancetaylor
Copy link
Contributor

I'm willing to believe that ab_ is correct.

@gopherbot
Copy link

Change https://golang.org/cl/175980 mentions this issue: spec: clarify language on package-level variable initialization

gopherbot pushed a commit that referenced this issue May 13, 2019
The very first paragraph on "Package initialization" stated that
"variables are initialized in declaration order, but after any
variables they might depend on". This phrasing was easily
misread as "declaration order is the first sorting criteria"
and then contradicted what the subsequent paragraphs spelled
out in precise detail.

Instead, variable initialization proceeds by repeatedly determining
a set of ready to initialize variables, and then selecting from that
set the variable declared earliest. That is, declaration order is the
second sorting criteria.

Also, for the purpose of variable initialization, declarations
introducing blank (_) variables are considered like any other
variables (their initialization expressions may have side-effects
and affect initialization order), even though blank identifiers
are not "declared".

This CL adds clarifying language regarding these two issues
and the supporting example.

Both gccgo and go/types implement this behavior. cmd/compile
has a long-standing issue (#22326).

The spec also did not state in which order multiple variables
initialized by a single (multi-value) initialization expression are
handled. This CL adds a clarifying paragraph: If any such variable
is initialized, all that declaration's variables are initialized at
the same time.

This behavior matches user expectation: We are not expecting to
observe partially initialized sets of variables in declarations
such as "var a, b, c = f()".

It also matches existing cmd/compile and go/types (but not gccgo)
behavior.

Finally, cmd/compile, gccgo, and go/types produce different
initialization orders in (esoteric) cases where hidden (not
detected with existing rules) dependencies exist. Added a
sentence and example clarifying how much leeway compilers have
in those situations. The goal is to preserve the ability to
use static initialization while at the same time maintain
the relative initialization order of variables with detected
dependencies.

Fixes   #31292.
Updates #22326.

Change-Id: I0a369abff8cfce27afc975998db875f5c580caa2
Reviewed-on: https://go-review.googlesource.com/c/go/+/175980
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
@bradfitz
Copy link
Contributor

@griesemer, what remains here?

@mdempsky
Copy link
Member

@bradfitz I need to update my cmd/compile CL. I was waiting until we'd clarified the ambiguity in #31292, which is done now.

@mdempsky
Copy link
Member

CL 170062 is ready for review, if it's not too late in the cycle.

@griesemer
Copy link
Contributor

griesemer commented May 21, 2019 via email

@gopherbot
Copy link

Change https://golang.org/cl/179238 mentions this issue: cmd/compile: sort OAS2* declarations

@gopherbot
Copy link

Change https://golang.org/cl/179398 mentions this issue: cmd/compile: fix fmt_test.go after CL 170062

gopherbot pushed a commit that referenced this issue May 29, 2019
Updates #22326.

Change-Id: Ia9173b6eb29b2a4f90f4ba39bf53b6e9b7a6d6bf
Reviewed-on: https://go-review.googlesource.com/c/go/+/179398
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
@golang golang locked and limited conversation to collaborators May 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
early-in-cycle A change that should be done early in the 3 month dev cycle. FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done. release-blocker
Projects
None yet
Development

No branches or pull requests

8 participants