Source file src/cmd/compile/internal/ssa/compile.go

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package ssa
     6  
     7  import (
     8  	"cmd/internal/src"
     9  	"fmt"
    10  	"hash/crc32"
    11  	"internal/buildcfg"
    12  	"io"
    13  	"log"
    14  	"math/rand"
    15  	"os"
    16  	"path/filepath"
    17  	"regexp"
    18  	"runtime"
    19  	"sort"
    20  	"strings"
    21  	"time"
    22  )
    23  
    24  // Compile is the main entry point for this package.
    25  // Compile modifies f so that on return:
    26  //   - all Values in f map to 0 or 1 assembly instructions of the target architecture
    27  //   - the order of f.Blocks is the order to emit the Blocks
    28  //   - the order of b.Values is the order to emit the Values in each Block
    29  //   - f has a non-nil regAlloc field
    30  func Compile(f *Func) {
    31  	// TODO: debugging - set flags to control verbosity of compiler,
    32  	// which phases to dump IR before/after, etc.
    33  	if f.Log() {
    34  		f.Logf("compiling %s\n", f.Name)
    35  	}
    36  
    37  	var rnd *rand.Rand
    38  	if checkEnabled {
    39  		seed := int64(crc32.ChecksumIEEE(([]byte)(f.Name))) ^ int64(checkRandSeed)
    40  		rnd = rand.New(rand.NewSource(seed))
    41  	}
    42  
    43  	// hook to print function & phase if panic happens
    44  	phaseName := "init"
    45  	defer func() {
    46  		if phaseName != "" {
    47  			err := recover()
    48  			stack := make([]byte, 16384)
    49  			n := runtime.Stack(stack, false)
    50  			stack = stack[:n]
    51  			if f.HTMLWriter != nil {
    52  				f.HTMLWriter.flushPhases()
    53  			}
    54  			f.Fatalf("panic during %s while compiling %s:\n\n%v\n\n%s\n", phaseName, f.Name, err, stack)
    55  		}
    56  	}()
    57  
    58  	// Run all the passes
    59  	if f.Log() {
    60  		printFunc(f)
    61  	}
    62  	f.HTMLWriter.WritePhase("start", "start")
    63  	if BuildDump[f.Name] {
    64  		f.dumpFile("build")
    65  	}
    66  	if checkEnabled {
    67  		checkFunc(f)
    68  	}
    69  	const logMemStats = false
    70  	for _, p := range passes {
    71  		if !f.Config.optimize && !p.required || p.disabled {
    72  			continue
    73  		}
    74  		f.pass = &p
    75  		phaseName = p.name
    76  		if f.Log() {
    77  			f.Logf("  pass %s begin\n", p.name)
    78  		}
    79  		// TODO: capture logging during this pass, add it to the HTML
    80  		var mStart runtime.MemStats
    81  		if logMemStats || p.mem {
    82  			runtime.ReadMemStats(&mStart)
    83  		}
    84  
    85  		if checkEnabled && !f.scheduled {
    86  			// Test that we don't depend on the value order, by randomizing
    87  			// the order of values in each block. See issue 18169.
    88  			for _, b := range f.Blocks {
    89  				for i := 0; i < len(b.Values)-1; i++ {
    90  					j := i + rnd.Intn(len(b.Values)-i)
    91  					b.Values[i], b.Values[j] = b.Values[j], b.Values[i]
    92  				}
    93  			}
    94  		}
    95  
    96  		tStart := time.Now()
    97  		p.fn(f)
    98  		tEnd := time.Now()
    99  
   100  		// Need something less crude than "Log the whole intermediate result".
   101  		if f.Log() || f.HTMLWriter != nil {
   102  			time := tEnd.Sub(tStart).Nanoseconds()
   103  			var stats string
   104  			if logMemStats {
   105  				var mEnd runtime.MemStats
   106  				runtime.ReadMemStats(&mEnd)
   107  				nBytes := mEnd.TotalAlloc - mStart.TotalAlloc
   108  				nAllocs := mEnd.Mallocs - mStart.Mallocs
   109  				stats = fmt.Sprintf("[%d ns %d allocs %d bytes]", time, nAllocs, nBytes)
   110  			} else {
   111  				stats = fmt.Sprintf("[%d ns]", time)
   112  			}
   113  
   114  			if f.Log() {
   115  				f.Logf("  pass %s end %s\n", p.name, stats)
   116  				printFunc(f)
   117  			}
   118  			f.HTMLWriter.WritePhase(phaseName, fmt.Sprintf("%s <span class=\"stats\">%s</span>", phaseName, stats))
   119  		}
   120  		if p.time || p.mem {
   121  			// Surround timing information w/ enough context to allow comparisons.
   122  			time := tEnd.Sub(tStart).Nanoseconds()
   123  			if p.time {
   124  				f.LogStat("TIME(ns)", time)
   125  			}
   126  			if p.mem {
   127  				var mEnd runtime.MemStats
   128  				runtime.ReadMemStats(&mEnd)
   129  				nBytes := mEnd.TotalAlloc - mStart.TotalAlloc
   130  				nAllocs := mEnd.Mallocs - mStart.Mallocs
   131  				f.LogStat("TIME(ns):BYTES:ALLOCS", time, nBytes, nAllocs)
   132  			}
   133  		}
   134  		if p.dump != nil && p.dump[f.Name] {
   135  			// Dump function to appropriately named file
   136  			f.dumpFile(phaseName)
   137  		}
   138  		if checkEnabled {
   139  			checkFunc(f)
   140  		}
   141  	}
   142  
   143  	if f.HTMLWriter != nil {
   144  		// Ensure we write any pending phases to the html
   145  		f.HTMLWriter.flushPhases()
   146  	}
   147  
   148  	if f.ruleMatches != nil {
   149  		var keys []string
   150  		for key := range f.ruleMatches {
   151  			keys = append(keys, key)
   152  		}
   153  		sort.Strings(keys)
   154  		buf := new(strings.Builder)
   155  		fmt.Fprintf(buf, "%s: ", f.Name)
   156  		for _, key := range keys {
   157  			fmt.Fprintf(buf, "%s=%d ", key, f.ruleMatches[key])
   158  		}
   159  		fmt.Fprint(buf, "\n")
   160  		fmt.Print(buf.String())
   161  	}
   162  
   163  	// Squash error printing defer
   164  	phaseName = ""
   165  }
   166  
   167  // DumpFileForPhase creates a file from the function name and phase name,
   168  // warning and returning nil if this is not possible.
   169  func (f *Func) DumpFileForPhase(phaseName string) io.WriteCloser {
   170  	f.dumpFileSeq++
   171  	fname := fmt.Sprintf("%s_%02d__%s.dump", f.Name, int(f.dumpFileSeq), phaseName)
   172  	fname = strings.Replace(fname, " ", "_", -1)
   173  	fname = strings.Replace(fname, "/", "_", -1)
   174  	fname = strings.Replace(fname, ":", "_", -1)
   175  
   176  	if ssaDir := os.Getenv("GOSSADIR"); ssaDir != "" {
   177  		fname = filepath.Join(ssaDir, fname)
   178  	}
   179  
   180  	fi, err := os.Create(fname)
   181  	if err != nil {
   182  		f.Warnl(src.NoXPos, "Unable to create after-phase dump file %s", fname)
   183  		return nil
   184  	}
   185  	return fi
   186  }
   187  
   188  // dumpFile creates a file from the phase name and function name
   189  // Dumping is done to files to avoid buffering huge strings before
   190  // output.
   191  func (f *Func) dumpFile(phaseName string) {
   192  	fi := f.DumpFileForPhase(phaseName)
   193  	if fi != nil {
   194  		p := stringFuncPrinter{w: fi}
   195  		fprintFunc(p, f)
   196  		fi.Close()
   197  	}
   198  }
   199  
   200  type pass struct {
   201  	name     string
   202  	fn       func(*Func)
   203  	required bool
   204  	disabled bool
   205  	time     bool            // report time to run pass
   206  	mem      bool            // report mem stats to run pass
   207  	stats    int             // pass reports own "stats" (e.g., branches removed)
   208  	debug    int             // pass performs some debugging. =1 should be in error-testing-friendly Warnl format.
   209  	test     int             // pass-specific ad-hoc option, perhaps useful in development
   210  	dump     map[string]bool // dump if function name matches
   211  }
   212  
   213  func (p *pass) addDump(s string) {
   214  	if p.dump == nil {
   215  		p.dump = make(map[string]bool)
   216  	}
   217  	p.dump[s] = true
   218  }
   219  
   220  func (p *pass) String() string {
   221  	if p == nil {
   222  		return "nil pass"
   223  	}
   224  	return p.name
   225  }
   226  
   227  // Run consistency checker between each phase
   228  var (
   229  	checkEnabled  = false
   230  	checkRandSeed = 0
   231  )
   232  
   233  // Debug output
   234  var IntrinsicsDebug int
   235  var IntrinsicsDisable bool
   236  
   237  var BuildDebug int
   238  var BuildTest int
   239  var BuildStats int
   240  var BuildDump map[string]bool = make(map[string]bool) // names of functions to dump after initial build of ssa
   241  
   242  var GenssaDump map[string]bool = make(map[string]bool) // names of functions to dump after ssa has been converted to asm
   243  
   244  // PhaseOption sets the specified flag in the specified ssa phase,
   245  // returning empty string if this was successful or a string explaining
   246  // the error if it was not.
   247  // A version of the phase name with "_" replaced by " " is also checked for a match.
   248  // If the phase name begins a '~' then the rest of the underscores-replaced-with-blanks
   249  // version is used as a regular expression to match the phase name(s).
   250  //
   251  // Special cases that have turned out to be useful:
   252  //   - ssa/check/on enables checking after each phase
   253  //   - ssa/all/time enables time reporting for all phases
   254  //
   255  // See gc/lex.go for dissection of the option string.
   256  // Example uses:
   257  //
   258  // GO_GCFLAGS=-d=ssa/generic_cse/time,ssa/generic_cse/stats,ssa/generic_cse/debug=3 ./make.bash
   259  //
   260  // BOOT_GO_GCFLAGS=-d='ssa/~^.*scc$/off' GO_GCFLAGS='-d=ssa/~^.*scc$/off' ./make.bash
   261  func PhaseOption(phase, flag string, val int, valString string) string {
   262  	switch phase {
   263  	case "", "help":
   264  		lastcr := 0
   265  		phasenames := "    check, all, build, intrinsics, genssa"
   266  		for _, p := range passes {
   267  			pn := strings.Replace(p.name, " ", "_", -1)
   268  			if len(pn)+len(phasenames)-lastcr > 70 {
   269  				phasenames += "\n    "
   270  				lastcr = len(phasenames)
   271  				phasenames += pn
   272  			} else {
   273  				phasenames += ", " + pn
   274  			}
   275  		}
   276  		return `PhaseOptions usage:
   277  
   278      go tool compile -d=ssa/<phase>/<flag>[=<value>|<function_name>]
   279  
   280  where:
   281  
   282  - <phase> is one of:
   283  ` + phasenames + `
   284  
   285  - <flag> is one of:
   286      on, off, debug, mem, time, test, stats, dump, seed
   287  
   288  - <value> defaults to 1
   289  
   290  - <function_name> is required for the "dump" flag, and specifies the
   291    name of function to dump after <phase>
   292  
   293  Phase "all" supports flags "time", "mem", and "dump".
   294  Phase "intrinsics" supports flags "on", "off", and "debug".
   295  Phase "genssa" (assembly generation) supports the flag "dump".
   296  
   297  If the "dump" flag is specified, the output is written on a file named
   298  <phase>__<function_name>_<seq>.dump; otherwise it is directed to stdout.
   299  
   300  Examples:
   301  
   302      -d=ssa/check/on
   303  enables checking after each phase
   304  
   305  	-d=ssa/check/seed=1234
   306  enables checking after each phase, using 1234 to seed the PRNG
   307  used for value order randomization
   308  
   309      -d=ssa/all/time
   310  enables time reporting for all phases
   311  
   312      -d=ssa/prove/debug=2
   313  sets debugging level to 2 in the prove pass
   314  
   315  Be aware that when "/debug=X" is applied to a pass, some passes
   316  will emit debug output for all functions, and other passes will
   317  only emit debug output for functions that match the current
   318  GOSSAFUNC value.
   319  
   320  Multiple flags can be passed at once, by separating them with
   321  commas. For example:
   322  
   323      -d=ssa/check/on,ssa/all/time
   324  `
   325  	}
   326  
   327  	if phase == "check" {
   328  		switch flag {
   329  		case "on":
   330  			checkEnabled = val != 0
   331  			debugPoset = checkEnabled // also turn on advanced self-checking in prove's data structure
   332  			return ""
   333  		case "off":
   334  			checkEnabled = val == 0
   335  			debugPoset = checkEnabled
   336  			return ""
   337  		case "seed":
   338  			checkEnabled = true
   339  			checkRandSeed = val
   340  			debugPoset = checkEnabled
   341  			return ""
   342  		}
   343  	}
   344  
   345  	alltime := false
   346  	allmem := false
   347  	alldump := false
   348  	if phase == "all" {
   349  		switch flag {
   350  		case "time":
   351  			alltime = val != 0
   352  		case "mem":
   353  			allmem = val != 0
   354  		case "dump":
   355  			alldump = val != 0
   356  			if alldump {
   357  				BuildDump[valString] = true
   358  				GenssaDump[valString] = true
   359  			}
   360  		default:
   361  			return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/all/{time,mem,dump=function_name})", flag, phase)
   362  		}
   363  	}
   364  
   365  	if phase == "intrinsics" {
   366  		switch flag {
   367  		case "on":
   368  			IntrinsicsDisable = val == 0
   369  		case "off":
   370  			IntrinsicsDisable = val != 0
   371  		case "debug":
   372  			IntrinsicsDebug = val
   373  		default:
   374  			return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/intrinsics/{on,off,debug})", flag, phase)
   375  		}
   376  		return ""
   377  	}
   378  	if phase == "build" {
   379  		switch flag {
   380  		case "debug":
   381  			BuildDebug = val
   382  		case "test":
   383  			BuildTest = val
   384  		case "stats":
   385  			BuildStats = val
   386  		case "dump":
   387  			BuildDump[valString] = true
   388  		default:
   389  			return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/build/{debug,test,stats,dump=function_name})", flag, phase)
   390  		}
   391  		return ""
   392  	}
   393  	if phase == "genssa" {
   394  		switch flag {
   395  		case "dump":
   396  			GenssaDump[valString] = true
   397  		default:
   398  			return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/genssa/dump=function_name)", flag, phase)
   399  		}
   400  		return ""
   401  	}
   402  
   403  	underphase := strings.Replace(phase, "_", " ", -1)
   404  	var re *regexp.Regexp
   405  	if phase[0] == '~' {
   406  		r, ok := regexp.Compile(underphase[1:])
   407  		if ok != nil {
   408  			return fmt.Sprintf("Error %s in regexp for phase %s, flag %s", ok.Error(), phase, flag)
   409  		}
   410  		re = r
   411  	}
   412  	matchedOne := false
   413  	for i, p := range passes {
   414  		if phase == "all" {
   415  			p.time = alltime
   416  			p.mem = allmem
   417  			if alldump {
   418  				p.addDump(valString)
   419  			}
   420  			passes[i] = p
   421  			matchedOne = true
   422  		} else if p.name == phase || p.name == underphase || re != nil && re.MatchString(p.name) {
   423  			switch flag {
   424  			case "on":
   425  				p.disabled = val == 0
   426  			case "off":
   427  				p.disabled = val != 0
   428  			case "time":
   429  				p.time = val != 0
   430  			case "mem":
   431  				p.mem = val != 0
   432  			case "debug":
   433  				p.debug = val
   434  			case "stats":
   435  				p.stats = val
   436  			case "test":
   437  				p.test = val
   438  			case "dump":
   439  				p.addDump(valString)
   440  			default:
   441  				return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option", flag, phase)
   442  			}
   443  			if p.disabled && p.required {
   444  				return fmt.Sprintf("Cannot disable required SSA phase %s using -d=ssa/%s debug option", phase, phase)
   445  			}
   446  			passes[i] = p
   447  			matchedOne = true
   448  		}
   449  	}
   450  	if matchedOne {
   451  		return ""
   452  	}
   453  	return fmt.Sprintf("Did not find a phase matching %s in -d=ssa/... debug option", phase)
   454  }
   455  
   456  // list of passes for the compiler
   457  var passes = [...]pass{
   458  	{name: "number lines", fn: numberLines, required: true},
   459  	{name: "early phielim and copyelim", fn: copyelim},
   460  	{name: "early deadcode", fn: deadcode}, // remove generated dead code to avoid doing pointless work during opt
   461  	{name: "short circuit", fn: shortcircuit},
   462  	{name: "decompose user", fn: decomposeUser, required: true},
   463  	{name: "pre-opt deadcode", fn: deadcode},
   464  	{name: "opt", fn: opt, required: true},               // NB: some generic rules know the name of the opt pass. TODO: split required rules and optimizing rules
   465  	{name: "zero arg cse", fn: zcse, required: true},     // required to merge OpSB values
   466  	{name: "opt deadcode", fn: deadcode, required: true}, // remove any blocks orphaned during opt
   467  	{name: "generic cse", fn: cse},
   468  	{name: "phiopt", fn: phiopt},
   469  	{name: "gcse deadcode", fn: deadcode, required: true}, // clean out after cse and phiopt
   470  	{name: "nilcheckelim", fn: nilcheckelim},
   471  	{name: "prove", fn: prove},
   472  	{name: "early fuse", fn: fuseEarly},
   473  	{name: "expand calls", fn: expandCalls, required: true},
   474  	{name: "decompose builtin", fn: postExpandCallsDecompose, required: true},
   475  	{name: "softfloat", fn: softfloat, required: true},
   476  	{name: "late opt", fn: opt, required: true}, // TODO: split required rules and optimizing rules
   477  	{name: "dead auto elim", fn: elimDeadAutosGeneric},
   478  	{name: "sccp", fn: sccp},
   479  	{name: "generic deadcode", fn: deadcode, required: true}, // remove dead stores, which otherwise mess up store chain
   480  	{name: "check bce", fn: checkbce},
   481  	{name: "branchelim", fn: branchelim},
   482  	{name: "late fuse", fn: fuseLate},
   483  	{name: "dse", fn: dse},
   484  	{name: "memcombine", fn: memcombine},
   485  	{name: "writebarrier", fn: writebarrier, required: true}, // expand write barrier ops
   486  	{name: "insert resched checks", fn: insertLoopReschedChecks,
   487  		disabled: !buildcfg.Experiment.PreemptibleLoops}, // insert resched checks in loops.
   488  	{name: "lower", fn: lower, required: true},
   489  	{name: "addressing modes", fn: addressingModes, required: false},
   490  	{name: "late lower", fn: lateLower, required: true},
   491  	{name: "lowered deadcode for cse", fn: deadcode}, // deadcode immediately before CSE avoids CSE making dead values live again
   492  	{name: "lowered cse", fn: cse},
   493  	{name: "elim unread autos", fn: elimUnreadAutos},
   494  	{name: "tighten tuple selectors", fn: tightenTupleSelectors, required: true},
   495  	{name: "lowered deadcode", fn: deadcode, required: true},
   496  	{name: "checkLower", fn: checkLower, required: true},
   497  	{name: "late phielim and copyelim", fn: copyelim},
   498  	{name: "tighten", fn: tighten, required: true}, // move values closer to their uses
   499  	{name: "late deadcode", fn: deadcode},
   500  	{name: "critical", fn: critical, required: true}, // remove critical edges
   501  	{name: "phi tighten", fn: phiTighten},            // place rematerializable phi args near uses to reduce value lifetimes
   502  	{name: "likelyadjust", fn: likelyadjust},
   503  	{name: "layout", fn: layout, required: true},     // schedule blocks
   504  	{name: "schedule", fn: schedule, required: true}, // schedule values
   505  	{name: "late nilcheck", fn: nilcheckelim2},
   506  	{name: "flagalloc", fn: flagalloc, required: true}, // allocate flags register
   507  	{name: "regalloc", fn: regalloc, required: true},   // allocate int & float registers + stack slots
   508  	{name: "loop rotate", fn: loopRotate},
   509  	{name: "trim", fn: trim}, // remove empty blocks
   510  }
   511  
   512  // Double-check phase ordering constraints.
   513  // This code is intended to document the ordering requirements
   514  // between different phases. It does not override the passes
   515  // list above.
   516  type constraint struct {
   517  	a, b string // a must come before b
   518  }
   519  
   520  var passOrder = [...]constraint{
   521  	// "insert resched checks" uses mem, better to clean out stores first.
   522  	{"dse", "insert resched checks"},
   523  	// insert resched checks adds new blocks containing generic instructions
   524  	{"insert resched checks", "lower"},
   525  	{"insert resched checks", "tighten"},
   526  
   527  	// prove relies on common-subexpression elimination for maximum benefits.
   528  	{"generic cse", "prove"},
   529  	// deadcode after prove to eliminate all new dead blocks.
   530  	{"prove", "generic deadcode"},
   531  	// common-subexpression before dead-store elim, so that we recognize
   532  	// when two address expressions are the same.
   533  	{"generic cse", "dse"},
   534  	// cse substantially improves nilcheckelim efficacy
   535  	{"generic cse", "nilcheckelim"},
   536  	// allow deadcode to clean up after nilcheckelim
   537  	{"nilcheckelim", "generic deadcode"},
   538  	// nilcheckelim generates sequences of plain basic blocks
   539  	{"nilcheckelim", "late fuse"},
   540  	// nilcheckelim relies on opt to rewrite user nil checks
   541  	{"opt", "nilcheckelim"},
   542  	// tighten will be most effective when as many values have been removed as possible
   543  	{"generic deadcode", "tighten"},
   544  	{"generic cse", "tighten"},
   545  	// checkbce needs the values removed
   546  	{"generic deadcode", "check bce"},
   547  	// decompose builtin now also cleans up after expand calls
   548  	{"expand calls", "decompose builtin"},
   549  	// don't run optimization pass until we've decomposed builtin objects
   550  	{"decompose builtin", "late opt"},
   551  	// decompose builtin is the last pass that may introduce new float ops, so run softfloat after it
   552  	{"decompose builtin", "softfloat"},
   553  	// tuple selectors must be tightened to generators and de-duplicated before scheduling
   554  	{"tighten tuple selectors", "schedule"},
   555  	// remove critical edges before phi tighten, so that phi args get better placement
   556  	{"critical", "phi tighten"},
   557  	// don't layout blocks until critical edges have been removed
   558  	{"critical", "layout"},
   559  	// regalloc requires the removal of all critical edges
   560  	{"critical", "regalloc"},
   561  	// regalloc requires all the values in a block to be scheduled
   562  	{"schedule", "regalloc"},
   563  	// the rules in late lower run after the general rules.
   564  	{"lower", "late lower"},
   565  	// late lower may generate some values that need to be CSEed.
   566  	{"late lower", "lowered cse"},
   567  	// checkLower must run after lowering & subsequent dead code elim
   568  	{"lower", "checkLower"},
   569  	{"lowered deadcode", "checkLower"},
   570  	{"late lower", "checkLower"},
   571  	// late nilcheck needs instructions to be scheduled.
   572  	{"schedule", "late nilcheck"},
   573  	// flagalloc needs instructions to be scheduled.
   574  	{"schedule", "flagalloc"},
   575  	// regalloc needs flags to be allocated first.
   576  	{"flagalloc", "regalloc"},
   577  	// loopRotate will confuse regalloc.
   578  	{"regalloc", "loop rotate"},
   579  	// trim needs regalloc to be done first.
   580  	{"regalloc", "trim"},
   581  	// memcombine works better if fuse happens first, to help merge stores.
   582  	{"late fuse", "memcombine"},
   583  	// memcombine is a arch-independent pass.
   584  	{"memcombine", "lower"},
   585  }
   586  
   587  func init() {
   588  	for _, c := range passOrder {
   589  		a, b := c.a, c.b
   590  		i := -1
   591  		j := -1
   592  		for k, p := range passes {
   593  			if p.name == a {
   594  				i = k
   595  			}
   596  			if p.name == b {
   597  				j = k
   598  			}
   599  		}
   600  		if i < 0 {
   601  			log.Panicf("pass %s not found", a)
   602  		}
   603  		if j < 0 {
   604  			log.Panicf("pass %s not found", b)
   605  		}
   606  		if i >= j {
   607  			log.Panicf("passes %s and %s out of order", a, b)
   608  		}
   609  	}
   610  }
   611  

View as plain text