Repository: goplus/gop
Pull request #919 (2021-11-21 17:31:33)

Add modfile package which can be used to parse gop.mod
The syntax tree of this gop.mod file can be defined as

```golang
// A File is the parsed, interpreted form of a gop.mod file.
type File struct {
	Module    *Module
	Go        *Go
	Gop       *Gop
	Classfile *Classfile
	Register  *Register
	Require   []*Require
	Exclude   []*Exclude
	Replace   []*Replace
	Retract   []*Retract

	Syntax *FileSyntax
}

// A Gop is the gop statement.
type Gop struct {
	Version string // "1.23"
	Syntax  *Line
}

// A Classfile is the classfile statement.
type Classfile struct {
	ProjExt  string   // ".gmx"
	WorkExt  string   // ".spx"
	PkgPaths []string // paths of classfile
	Syntax   *Line
}

// A Register is the register statement.
type Register struct {
	ClassfileMod string // modname of classfile
	Syntax       *Line
}
```

If the content of the gop.mod file is

```
module spx

go 1.17
gop 1.1

classfile .gmx .spx github.com/goplus/spx math

require (
    github.com/ajstarks/svgo v0.0.0-20210927141636-6d70534b1098
)
```

gop.mod can be parsed as

```
File: {
    ...
    Gop: {
          Version: "1.1"
    }
    Classfile: {
           ProjExt: ".gmx", 
           WorkExt: "".spx""
           PkgPaths: ["github.com/goplus/spx", "math"]
    }
    ...
}
```

If the content of the gop.mod file is

```
module moduleUserProj

go 1.17
gop 1.1

register github.com/goplus/spx

require (
    github.com/goplus/spx v1.0
)
```

gop.mod can be parsed as

```
File: {
    ...
    Register: {
            ClassfileMod: "github.com/goplus/spx"
    }
    ...
}
```



Patch:
diff --git a/x/mod/internal/lazyregexp/lazyregexp.go b/x/mod/internal/lazyregexp/lazyregexp.go
new file mode 100644
index 00000000..7f386635
--- /dev/null
+++ b/x/mod/internal/lazyregexp/lazyregexp.go
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Package lazyregexp is a thin wrapper over regexp, allowing the use of global
+// regexp variables without forcing them to be compiled at init.
+package lazyregexp
+
+import (
+	"os"
+	"regexp"
+	"strings"
+	"sync"
+)
+
+// Regexp is a wrapper around regexp.Regexp, where the underlying regexp will be
+// compiled the first time it is needed.
+type Regexp struct {
+	str  string
+	once sync.Once
+	rx   *regexp.Regexp
+}
+
+func (r *Regexp) re() *regexp.Regexp {
+	r.once.Do(r.build)
+	return r.rx
+}
+
+func (r *Regexp) build() {
+	r.rx = regexp.MustCompile(r.str)
+	r.str = ""
+}
+
+func (r *Regexp) FindSubmatch(s []byte) [][]byte {
+	return r.re().FindSubmatch(s)
+}
+
+func (r *Regexp) FindStringSubmatch(s string) []string {
+	return r.re().FindStringSubmatch(s)
+}
+
+func (r *Regexp) FindStringSubmatchIndex(s string) []int {
+	return r.re().FindStringSubmatchIndex(s)
+}
+
+func (r *Regexp) ReplaceAllString(src, repl string) string {
+	return r.re().ReplaceAllString(src, repl)
+}
+
+func (r *Regexp) FindString(s string) string {
+	return r.re().FindString(s)
+}
+
+func (r *Regexp) FindAllString(s string, n int) []string {
+	return r.re().FindAllString(s, n)
+}
+
+func (r *Regexp) MatchString(s string) bool {
+	return r.re().MatchString(s)
+}
+
+func (r *Regexp) SubexpNames() []string {
+	return r.re().SubexpNames()
+}
+
+var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
+
+// New creates a new lazy regexp, delaying the compiling work until it is first
+// needed. If the code is being run as part of tests, the regexp compiling will
+// happen immediately.
+func New(str string) *Regexp {
+	lr := &Regexp{str: str}
+	if inTest {
+		// In tests, always compile the regexps early.
+		lr.re()
+	}
+	return lr
+}
diff --git a/x/mod/modfile/print.go b/x/mod/modfile/print.go
new file mode 100644
index 00000000..e38d9463
--- /dev/null
+++ b/x/mod/modfile/print.go
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package modfile
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+// Format returns a gop.mod file as a byte slice, formatted in standard style.
+func Format(f *FileSyntax) []byte {
+	pr := &printer{}
+	pr.file(f)
+	return pr.Bytes()
+}
+
+// A printer collects the state during printing of a file or expression.
+type printer struct {
+	bytes.Buffer           // output buffer
+	comment      []Comment // pending end-of-line comments
+	margin       int       // left margin (indent), a number of tabs
+}
+
+// printf prints to the buffer.
+func (p *printer) printf(format string, args ...interface{}) {
+	fmt.Fprintf(p, format, args...)
+}
+
+// indent returns the position on the current line, in bytes, 0-indexed.
+func (p *printer) indent() int {
+	b := p.Bytes()
+	n := 0
+	for n < len(b) && b[len(b)-1-n] != '\n' {
+		n++
+	}
+	return n
+}
+
+// newline ends the current line, flushing end-of-line comments.
+func (p *printer) newline() {
+	if len(p.comment) > 0 {
+		p.printf(" ")
+		for i, com := range p.comment {
+			if i > 0 {
+				p.trim()
+				p.printf("\n")
+				for i := 0; i < p.margin; i++ {
+					p.printf("\t")
+				}
+			}
+			p.printf("%s", strings.TrimSpace(com.Token))
+		}
+		p.comment = p.comment[:0]
+	}
+
+	p.trim()
+	p.printf("\n")
+	for i := 0; i < p.margin; i++ {
+		p.printf("\t")
+	}
+}
+
+// trim removes trailing spaces and tabs from the current line.
+func (p *printer) trim() {
+	// Remove trailing spaces and tabs from line we're about to end.
+	b := p.Bytes()
+	n := len(b)
+	for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') {
+		n--
+	}
+	p.Truncate(n)
+}
+
+// file formats the given file into the print buffer.
+func (p *printer) file(f *FileSyntax) {
+	for _, com := range f.Before {
+		p.printf("%s", strings.TrimSpace(com.Token))
+		p.newline()
+	}
+
+	for i, stmt := range f.Stmt {
+		switch x := stmt.(type) {
+		case *CommentBlock:
+			// comments already handled
+			p.expr(x)
+
+		default:
+			p.expr(x)
+			p.newline()
+		}
+
+		for _, com := range stmt.Comment().After {
+			p.printf("%s", strings.TrimSpace(com.Token))
+			p.newline()
+		}
+
+		if i+1 < len(f.Stmt) {
+			p.newline()
+		}
+	}
+}
+
+func (p *printer) expr(x Expr) {
+	// Emit line-comments preceding this expression.
+	if before := x.Comment().Before; len(before) > 0 {
+		// Want to print a line comment.
+		// Line comments must be at the current margin.
+		p.trim()
+		if p.indent() > 0 {
+			// There's other text on the line. Start a new line.
+			p.printf("\n")
+		}
+		// Re-indent to margin.
+		for i := 0; i < p.margin; i++ {
+			p.printf("\t")
+		}
+		for _, com := range before {
+			p.printf("%s", strings.TrimSpace(com.Token))
+			p.newline()
+		}
+	}
+
+	switch x := x.(type) {
+	default:
+		panic(fmt.Errorf("printer: unexpected type %T", x))
+
+	case *CommentBlock:
+		// done
+
+	case *LParen:
+		p.printf("(")
+	case *RParen:
+		p.printf(")")
+
+	case *Line:
+		p.tokens(x.Token)
+
+	case *LineBlock:
+		p.tokens(x.Token)
+		p.printf(" ")
+		p.expr(&x.LParen)
+		p.margin++
+		for _, l := range x.Line {
+			p.newline()
+			p.expr(l)
+		}
+		p.margin--
+		p.newline()
+		p.expr(&x.RParen)
+	}
+
+	// Queue end-of-line comments for printing when we
+	// reach the end of the line.
+	p.comment = append(p.comment, x.Comment().Suffix...)
+}
+
+func (p *printer) tokens(tokens []string) {
+	sep := ""
+	for _, t := range tokens {
+		if t == "," || t == ")" || t == "]" || t == "}" {
+			sep = ""
+		}
+		p.printf("%s%s", sep, t)
+		sep = " "
+		if t == "(" || t == "[" || t == "{" {
+			sep = ""
+		}
+	}
+}
diff --git a/x/mod/modfile/read.go b/x/mod/modfile/read.go
new file mode 100644
index 00000000..3cc9a1b3
--- /dev/null
+++ b/x/mod/modfile/read.go
@@ -0,0 +1,968 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package modfile
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A Position describes an arbitrary source position in a file, including the
+// file, line, column, and byte offset.
+type Position struct {
+	Line     int // line in input (starting at 1)
+	LineRune int // rune in line (starting at 1)
+	Byte     int // byte in input (starting at 0)
+}
+
+// add returns the position at the end of s, assuming it starts at p.
+func (p Position) add(s string) Position {
+	p.Byte += len(s)
+	if n := strings.Count(s, "\n"); n > 0 {
+		p.Line += n
+		s = s[strings.LastIndex(s, "\n")+1:]
+		p.LineRune = 1
+	}
+	p.LineRune += utf8.RuneCountInString(s)
+	return p
+}
+
+// An Expr represents an input element.
+type Expr interface {
+	// Span returns the start and end position of the expression,
+	// excluding leading or trailing comments.
+	Span() (start, end Position)
+
+	// Comment returns the comments attached to the expression.
+	// This method would normally be named 'Comments' but that
+	// would interfere with embedding a type of the same name.
+	Comment() *Comments
+}
+
+// A Comment represents a single // comment.
+type Comment struct {
+	Start  Position
+	Token  string // without trailing newline
+	Suffix bool   // an end of line (not whole line) comment
+}
+
+// Comments collects the comments associated with an expression.
+type Comments struct {
+	Before []Comment // whole-line comments before this expression
+	Suffix []Comment // end-of-line comments after this expression
+
+	// For top-level expressions only, After lists whole-line
+	// comments following the expression.
+	After []Comment
+}
+
+// Comment returns the receiver. This isn't useful by itself, but
+// a Comments struct is embedded into all the expression
+// implementation types, and this gives each of those a Comment
+// method to satisfy the Expr interface.
+func (c *Comments) Comment() *Comments {
+	return c
+}
+
+// A FileSyntax represents an entire gop.mod file.
+type FileSyntax struct {
+	Name string // file path
+	Comments
+	Stmt []Expr
+}
+
+func (x *FileSyntax) Span() (start, end Position) {
+	if len(x.Stmt) == 0 {
+		return
+	}
+	start, _ = x.Stmt[0].Span()
+	_, end = x.Stmt[len(x.Stmt)-1].Span()
+	return start, end
+}
+
+// addLine adds a line containing the given tokens to the file.
+//
+// If the first token of the hint matches the first token of the
+// line, the new line is added at the end of the block containing hint,
+// extracting hint into a new block if it is not yet in one.
+//
+// If the hint is non-nil buts its first token does not match,
+// the new line is added after the block containing hint
+// (or hint itself, if not in a block).
+//
+// If no hint is provided, addLine appends the line to the end of
+// the last block with a matching first token,
+// or to the end of the file if no such block exists.
+func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
+	if hint == nil {
+		// If no hint given, add to the last statement of the given type.
+	Loop:
+		for i := len(x.Stmt) - 1; i >= 0; i-- {
+			stmt := x.Stmt[i]
+			switch stmt := stmt.(type) {
+			case *Line:
+				if stmt.Token != nil && stmt.Token[0] == tokens[0] {
+					hint = stmt
+					break Loop
+				}
+			case *LineBlock:
+				if stmt.Token[0] == tokens[0] {
+					hint = stmt
+					break Loop
+				}
+			}
+		}
+	}
+
+	newLineAfter := func(i int) *Line {
+		new := &Line{Token: tokens}
+		if i == len(x.Stmt) {
+			x.Stmt = append(x.Stmt, new)
+		} else {
+			x.Stmt = append(x.Stmt, nil)
+			copy(x.Stmt[i+2:], x.Stmt[i+1:])
+			x.Stmt[i+1] = new
+		}
+		return new
+	}
+
+	if hint != nil {
+		for i, stmt := range x.Stmt {
+			switch stmt := stmt.(type) {
+			case *Line:
+				if stmt == hint {
+					if stmt.Token == nil || stmt.Token[0] != tokens[0] {
+						return newLineAfter(i)
+					}
+
+					// Convert line to line block.
+					stmt.InBlock = true
+					block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}}
+					stmt.Token = stmt.Token[1:]
+					x.Stmt[i] = block
+					new := &Line{Token: tokens[1:], InBlock: true}
+					block.Line = append(block.Line, new)
+					return new
+				}
+
+			case *LineBlock:
+				if stmt == hint {
+					if stmt.Token[0] != tokens[0] {
+						return newLineAfter(i)
+					}
+
+					new := &Line{Token: tokens[1:], InBlock: true}
+					stmt.Line = append(stmt.Line, new)
+					return new
+				}
+
+				for j, line := range stmt.Line {
+					if line == hint {
+						if stmt.Token[0] != tokens[0] {
+							return newLineAfter(i)
+						}
+
+						// Add new line after hint within the block.
+						stmt.Line = append(stmt.Line, nil)
+						copy(stmt.Line[j+2:], stmt.Line[j+1:])
+						new := &Line{Token: tokens[1:], InBlock: true}
+						stmt.Line[j+1] = new
+						return new
+					}
+				}
+			}
+		}
+	}
+
+	new := &Line{Token: tokens}
+	x.Stmt = append(x.Stmt, new)
+	return new
+}
+
+func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
+	if line.InBlock {
+		tokens = tokens[1:]
+	}
+	line.Token = tokens
+}
+
+func (x *FileSyntax) removeLine(line *Line) {
+	line.Token = nil
+}
+
+// Cleanup cleans up the file syntax x after any edit operations.
+// To avoid quadratic behavior, removeLine marks the line as dead
+// by setting line.Token = nil but does not remove it from the slice
+// in which it appears. After edits have all been indicated,
+// calling Cleanup cleans out the dead lines.
+func (x *FileSyntax) Cleanup() {
+	w := 0
+	for _, stmt := range x.Stmt {
+		switch stmt := stmt.(type) {
+		case *Line:
+			if stmt.Token == nil {
+				continue
+			}
+		case *LineBlock:
+			ww := 0
+			for _, line := range stmt.Line {
+				if line.Token != nil {
+					stmt.Line[ww] = line
+					ww++
+				}
+			}
+			if ww == 0 {
+				continue
+			}
+			if ww == 1 {
+				// Collapse block into single line.
+				line := &Line{
+					Comments: Comments{
+						Before: commentsAdd(stmt.Before, stmt.Line[0].Before),
+						Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix),
+						After:  commentsAdd(stmt.Line[0].After, stmt.After),
+					},
+					Token: stringsAdd(stmt.Token, stmt.Line[0].Token),
+				}
+				x.Stmt[w] = line
+				w++
+				continue
+			}
+			stmt.Line = stmt.Line[:ww]
+		}
+		x.Stmt[w] = stmt
+		w++
+	}
+	x.Stmt = x.Stmt[:w]
+}
+
+func commentsAdd(x, y []Comment) []Comment {
+	return append(x[:len(x):len(x)], y...)
+}
+
+func stringsAdd(x, y []string) []string {
+	return append(x[:len(x):len(x)], y...)
+}
+
+// A CommentBlock represents a top-level block of comments separate
+// from any rule.
+type CommentBlock struct {
+	Comments
+	Start Position
+}
+
+func (x *CommentBlock) Span() (start, end Position) {
+	return x.Start, x.Start
+}
+
+// A Line is a single line of tokens.
+type Line struct {
+	Comments
+	Start   Position
+	Token   []string
+	InBlock bool
+	End     Position
+}
+
+func (x *Line) Span() (start, end Position) {
+	return x.Start, x.End
+}
+
+// A LineBlock is a factored block of lines, like
+//
+//	require (
+//		"x"
+//		"y"
+//	)
+//
+type LineBlock struct {
+	Comments
+	Start  Position
+	LParen LParen
+	Token  []string
+	Line   []*Line
+	RParen RParen
+}
+
+func (x *LineBlock) Span() (start, end Position) {
+	return x.Start, x.RParen.Pos.add(")")
+}
+
+// An LParen represents the beginning of a parenthesized line block.
+// It is a place to store suffix comments.
+type LParen struct {
+	Comments
+	Pos Position
+}
+
+func (x *LParen) Span() (start, end Position) {
+	return x.Pos, x.Pos.add(")")
+}
+
+// An RParen represents the end of a parenthesized line block.
+// It is a place to store whole-line (before) comments.
+type RParen struct {
+	Comments
+	Pos Position
+}
+
+func (x *RParen) Span() (start, end Position) {
+	return x.Pos, x.Pos.add(")")
+}
+
+// An input represents a single input file being parsed.
+type input struct {
+	// Lexing state.
+	filename   string    // name of input file, for errors
+	complete   []byte    // entire input
+	remaining  []byte    // remaining input
+	tokenStart []byte    // token being scanned to end of input
+	token      token     // next token to be returned by lex, peek
+	pos        Position  // current input position
+	comments   []Comment // accumulated comments
+
+	// Parser state.
+	file        *FileSyntax // returned top-level syntax tree
+	parseErrors ErrorList   // errors encountered during parsing
+
+	// Comment assignment state.
+	pre  []Expr // all expressions, in preorder traversal
+	post []Expr // all expressions, in postorder traversal
+}
+
+func newInput(filename string, data []byte) *input {
+	return &input{
+		filename:  filename,
+		complete:  data,
+		remaining: data,
+		pos:       Position{Line: 1, LineRune: 1, Byte: 0},
+	}
+}
+
+// parse parses the input file.
+func parse(file string, data []byte) (f *FileSyntax, err error) {
+	// The parser panics for both routine errors like syntax errors
+	// and for programmer bugs like array index errors.
+	// Turn both into error returns. Catching bug panics is
+	// especially important when processing many files.
+	in := newInput(file, data)
+	defer func() {
+		if e := recover(); e != nil && e != &in.parseErrors {
+			in.parseErrors = append(in.parseErrors, Error{
+				Filename: in.filename,
+				Pos:      in.pos,
+				Err:      fmt.Errorf("internal error: %v", e),
+			})
+		}
+		if err == nil && len(in.parseErrors) > 0 {
+			err = in.parseErrors
+		}
+	}()
+
+	// Prime the lexer by reading in the first token. It will be available
+	// in the next peek() or lex() call.
+	in.readToken()
+
+	// Invoke the parser.
+	in.parseFile()
+	if len(in.parseErrors) > 0 {
+		return nil, in.parseErrors
+	}
+	in.file.Name = in.filename
+
+	// Assign comments to nearby syntax.
+	in.assignComments()
+
+	return in.file, nil
+}
+
+// Error is called to report an error.
+// Error does not return: it panics.
+func (in *input) Error(s string) {
+	in.parseErrors = append(in.parseErrors, Error{
+		Filename: in.filename,
+		Pos:      in.pos,
+		Err:      errors.New(s),
+	})
+	panic(&in.parseErrors)
+}
+
+// eof reports whether the input has reached end of file.
+func (in *input) eof() bool {
+	return len(in.remaining) == 0
+}
+
+// peekRune returns the next rune in the input without consuming it.
+func (in *input) peekRune() int {
+	if len(in.remaining) == 0 {
+		return 0
+	}
+	r, _ := utf8.DecodeRune(in.remaining)
+	return int(r)
+}
+
+// peekPrefix reports whether the remaining input begins with the given prefix.
+func (in *input) peekPrefix(prefix string) bool {
+	// This is like bytes.HasPrefix(in.remaining, []byte(prefix))
+	// but without the allocation of the []byte copy of prefix.
+	for i := 0; i < len(prefix); i++ {
+		if i >= len(in.remaining) || in.remaining[i] != prefix[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// readRune consumes and returns the next rune in the input.
+func (in *input) readRune() int {
+	if len(in.remaining) == 0 {
+		in.Error("internal lexer error: readRune at EOF")
+	}
+	r, size := utf8.DecodeRune(in.remaining)
+	in.remaining = in.remaining[size:]
+	if r == '\n' {
+		in.pos.Line++
+		in.pos.LineRune = 1
+	} else {
+		in.pos.LineRune++
+	}
+	in.pos.Byte += size
+	return int(r)
+}
+
+type token struct {
+	kind   tokenKind
+	pos    Position
+	endPos Position
+	text   string
+}
+
+type tokenKind int
+
+const (
+	_EOF tokenKind = -(iota + 1)
+	_EOLCOMMENT
+	_IDENT
+	_STRING
+	_COMMENT
+
+	// newlines and punctuation tokens are allowed as ASCII codes.
+)
+
+func (k tokenKind) isComment() bool {
+	return k == _COMMENT || k == _EOLCOMMENT
+}
+
+// isEOL returns whether a token terminates a line.
+func (k tokenKind) isEOL() bool {
+	return k == _EOF || k == _EOLCOMMENT || k == '\n'
+}
+
+// startToken marks the beginning of the next input token.
+// It must be followed by a call to endToken, once the token's text has
+// been consumed using readRune.
+func (in *input) startToken() {
+	in.tokenStart = in.remaining
+	in.token.text = ""
+	in.token.pos = in.pos
+}
+
+// endToken marks the end of an input token.
+// It records the actual token string in tok.text.
+// A single trailing newline (LF or CRLF) will be removed from comment tokens.
+func (in *input) endToken(kind tokenKind) {
+	in.token.kind = kind
+	text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)])
+	if kind.isComment() {
+		if strings.HasSuffix(text, "\r\n") {
+			text = text[:len(text)-2]
+		} else {
+			text = strings.TrimSuffix(text, "\n")
+		}
+	}
+	in.token.text = text
+	in.token.endPos = in.pos
+}
+
+// peek returns the kind of the the next token returned by lex.
+func (in *input) peek() tokenKind {
+	return in.token.kind
+}
+
+// lex is called from the parser to obtain the next input token.
+func (in *input) lex() token {
+	tok := in.token
+	in.readToken()
+	return tok
+}
+
+// readToken lexes the next token from the text and stores it in in.token.
+func (in *input) readToken() {
+	// Skip past spaces, stopping at non-space or EOF.
+	for !in.eof() {
+		c := in.peekRune()
+		if c == ' ' || c == '\t' || c == '\r' {
+			in.readRune()
+			continue
+		}
+
+		// Comment runs to end of line.
+		if in.peekPrefix("//") {
+			in.startToken()
+
+			// Is this comment the only thing on its line?
+			// Find the last \n before this // and see if it's all
+			// spaces from there to here.
+			i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n"))
+			suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0
+			in.readRune()
+			in.readRune()
+
+			// Consume comment.
+			for len(in.remaining) > 0 && in.readRune() != '\n' {
+			}
+
+			// If we are at top level (not in a statement), hand the comment to
+			// the parser as a _COMMENT token. The grammar is written
+			// to handle top-level comments itself.
+			if !suffix {
+				in.endToken(_COMMENT)
+				return
+			}
+
+			// Otherwise, save comment for later attachment to syntax tree.
+			in.endToken(_EOLCOMMENT)
+			in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix})
+			return
+		}
+
+		if in.peekPrefix("/*") {
+			in.Error("mod files must use // comments (not /* */ comments)")
+		}
+
+		// Found non-space non-comment.
+		break
+	}
+
+	// Found the beginning of the next token.
+	in.startToken()
+
+	// End of file.
+	if in.eof() {
+		in.endToken(_EOF)
+		return
+	}
+
+	// Punctuation tokens.
+	switch c := in.peekRune(); c {
+	case '\n', '(', ')', '[', ']', '{', '}', ',':
+		in.readRune()
+		in.endToken(tokenKind(c))
+		return
+
+	case '"', '`': // quoted string
+		quote := c
+		in.readRune()
+		for {
+			if in.eof() {
+				in.pos = in.token.pos
+				in.Error("unexpected EOF in string")
+			}
+			if in.peekRune() == '\n' {
+				in.Error("unexpected newline in string")
+			}
+			c := in.readRune()
+			if c == quote {
+				break
+			}
+			if c == '\\' && quote != '`' {
+				if in.eof() {
+					in.pos = in.token.pos
+					in.Error("unexpected EOF in string")
+				}
+				in.readRune()
+			}
+		}
+		in.endToken(_STRING)
+		return
+	}
+
+	// Checked all punctuation. Must be identifier token.
+	if c := in.peekRune(); !isIdent(c) {
+		in.Error(fmt.Sprintf("unexpected input character %#q", c))
+	}
+
+	// Scan over identifier.
+	for isIdent(in.peekRune()) {
+		if in.peekPrefix("//") {
+			break
+		}
+		if in.peekPrefix("/*") {
+			in.Error("mod files must use // comments (not /* */ comments)")
+		}
+		in.readRune()
+	}
+	in.endToken(_IDENT)
+}
+
+// isIdent reports whether c is an identifier rune.
+// We treat most printable runes as identifier runes, except for a handful of
+// ASCII punctuation characters.
+func isIdent(c int) bool {
+	switch r := rune(c); r {
+	case ' ', '(', ')', '[', ']', '{', '}', ',':
+		return false
+	default:
+		return !unicode.IsSpace(r) && unicode.IsPrint(r)
+	}
+}
+
+// Comment assignment.
+// We build two lists of all subexpressions, preorder and postorder.
+// The preorder list is ordered by start location, with outer expressions first.
+// The postorder list is ordered by end location, with outer expressions last.
+// We use the preorder list to assign each whole-line comment to the syntax
+// immediately following it, and we use the postorder list to assign each
+// end-of-line comment to the syntax immediately preceding it.
+
+// order walks the expression adding it and its subexpressions to the
+// preorder and postorder lists.
+func (in *input) order(x Expr) {
+	if x != nil {
+		in.pre = append(in.pre, x)
+	}
+	switch x := x.(type) {
+	default:
+		panic(fmt.Errorf("order: unexpected type %T", x))
+	case nil:
+		// nothing
+	case *LParen, *RParen:
+		// nothing
+	case *CommentBlock:
+		// nothing
+	case *Line:
+		// nothing
+	case *FileSyntax:
+		for _, stmt := range x.Stmt {
+			in.order(stmt)
+		}
+	case *LineBlock:
+		in.order(&x.LParen)
+		for _, l := range x.Line {
+			in.order(l)
+		}
+		in.order(&x.RParen)
+	}
+	if x != nil {
+		in.post = append(in.post, x)
+	}
+}
+
+// assignComments attaches comments to nearby syntax.
+func (in *input) assignComments() {
+	const debug = false
+
+	// Generate preorder and postorder lists.
+	in.order(in.file)
+
+	// Split into whole-line comments and suffix comments.
+	var line, suffix []Comment
+	for _, com := range in.comments {
+		if com.Suffix {
+			suffix = append(suffix, com)
+		} else {
+			line = append(line, com)
+		}
+	}
+
+	if debug {
+		for _, c := range line {
+			fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
+		}
+	}
+
+	// Assign line comments to syntax immediately following.
+	for _, x := range in.pre {
+		start, _ := x.Span()
+		if debug {
+			fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte)
+		}
+		xcom := x.Comment()
+		for len(line) > 0 && start.Byte >= line[0].Start.Byte {
+			if debug {
+				fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte)
+			}
+			xcom.Before = append(xcom.Before, line[0])
+			line = line[1:]
+		}
+	}
+
+	// Remaining line comments go at end of file.
+	in.file.After = append(in.file.After, line...)
+
+	if debug {
+		for _, c := range suffix {
+			fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
+		}
+	}
+
+	// Assign suffix comments to syntax immediately before.
+	for i := len(in.post) - 1; i >= 0; i-- {
+		x := in.post[i]
+
+		start, end := x.Span()
+		if debug {
+			fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
+		}
+
+		// Do not assign suffix comments to end of line block or whole file.
+		// Instead assign them to the last element inside.
+		switch x.(type) {
+		case *FileSyntax:
+			continue
+		}
+
+		// Do not assign suffix comments to something that starts
+		// on an earlier line, so that in
+		//
+		//	x ( y
+		//		z ) // comment
+		//
+		// we assign the comment to z and not to x ( ... ).
+		if start.Line != end.Line {
+			continue
+		}
+		xcom := x.Comment()
+		for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte {
+			if debug {
+				fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte)
+			}
+			xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1])
+			suffix = suffix[:len(suffix)-1]
+		}
+	}
+
+	// We assigned suffix comments in reverse.
+	// If multiple suffix comments were appended to the same
+	// expression node, they are now in reverse. Fix that.
+	for _, x := range in.post {
+		reverseComments(x.Comment().Suffix)
+	}
+
+	// Remaining suffix comments go at beginning of file.
+	in.file.Before = append(in.file.Before, suffix...)
+}
+
+// reverseComments reverses the []Comment list.
+func reverseComments(list []Comment) {
+	for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
+		list[i], list[j] = list[j], list[i]
+	}
+}
+
+func (in *input) parseFile() {
+	in.file = new(FileSyntax)
+	var cb *CommentBlock
+	for {
+		switch in.peek() {
+		case '\n':
+			in.lex()
+			if cb != nil {
+				in.file.Stmt = append(in.file.Stmt, cb)
+				cb = nil
+			}
+		case _COMMENT:
+			tok := in.lex()
+			if cb == nil {
+				cb = &CommentBlock{Start: tok.pos}
+			}
+			com := cb.Comment()
+			com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text})
+		case _EOF:
+			if cb != nil {
+				in.file.Stmt = append(in.file.Stmt, cb)
+			}
+			return
+		default:
+			in.parseStmt()
+			if cb != nil {
+				in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before
+				cb = nil
+			}
+		}
+	}
+}
+
+func (in *input) parseStmt() {
+	tok := in.lex()
+	start := tok.pos
+	end := tok.endPos
+	tokens := []string{tok.text}
+	for {
+		tok := in.lex()
+		switch {
+		case tok.kind.isEOL():
+			in.file.Stmt = append(in.file.Stmt, &Line{
+				Start: start,
+				Token: tokens,
+				End:   end,
+			})
+			return
+
+		case tok.kind == '(':
+			if next := in.peek(); next.isEOL() {
+				// Start of block: no more tokens on this line.
+				in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok))
+				return
+			} else if next == ')' {
+				rparen := in.lex()
+				if in.peek().isEOL() {
+					// Empty block.
+					in.lex()
+					in.file.Stmt = append(in.file.Stmt, &LineBlock{
+						Start:  start,
+						Token:  tokens,
+						LParen: LParen{Pos: tok.pos},
+						RParen: RParen{Pos: rparen.pos},
+					})
+					return
+				}
+				// '( )' in the middle of the line, not a block.
+				tokens = append(tokens, tok.text, rparen.text)
+			} else {
+				// '(' in the middle of the line, not a block.
+				tokens = append(tokens, tok.text)
+			}
+
+		default:
+			tokens = append(tokens, tok.text)
+			end = tok.endPos
+		}
+	}
+}
+
+func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock {
+	x := &LineBlock{
+		Start:  start,
+		Token:  token,
+		LParen: LParen{Pos: lparen.pos},
+	}
+	var comments []Comment
+	for {
+		switch in.peek() {
+		case _EOLCOMMENT:
+			// Suffix comment, will be attached later by assignComments.
+			in.lex()
+		case '\n':
+			// Blank line. Add an empty comment to preserve it.
+			in.lex()
+			if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" {
+				comments = append(comments, Comment{})
+			}
+		case _COMMENT:
+			tok := in.lex()
+			comments = append(comments, Comment{Start: tok.pos, Token: tok.text})
+		case _EOF:
+			in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune))
+		case ')':
+			rparen := in.lex()
+			x.RParen.Before = comments
+			x.RParen.Pos = rparen.pos
+			if !in.peek().isEOL() {
+				in.Error("syntax error (expected newline after closing paren)")
+			}
+			in.lex()
+			return x
+		default:
+			l := in.parseLine()
+			x.Line = append(x.Line, l)
+			l.Comment().Before = comments
+			comments = nil
+		}
+	}
+}
+
+func (in *input) parseLine() *Line {
+	tok := in.lex()
+	if tok.kind.isEOL() {
+		in.Error("internal parse error: parseLine at end of line")
+	}
+	start := tok.pos
+	end := tok.endPos
+	tokens := []string{tok.text}
+	for {
+		tok := in.lex()
+		if tok.kind.isEOL() {
+			return &Line{
+				Start:   start,
+				Token:   tokens,
+				End:     end,
+				InBlock: true,
+			}
+		}
+		tokens = append(tokens, tok.text)
+		end = tok.endPos
+	}
+}
+
+var (
+	slashSlash = []byte("//")
+	moduleStr  = []byte("module")
+)
+
+// ModulePath returns the module path from the gomod file text.
+// If it cannot find a module path, it returns an empty string.
+// It is tolerant of unrelated problems in the gop.mod file.
+func ModulePath(mod []byte) string {
+	for len(mod) > 0 {
+		line := mod
+		mod = nil
+		if i := bytes.IndexByte(line, '\n'); i >= 0 {
+			line, mod = line[:i], line[i+1:]
+		}
+		if i := bytes.Index(line, slashSlash); i >= 0 {
+			line = line[:i]
+		}
+		line = bytes.TrimSpace(line)
+		if !bytes.HasPrefix(line, moduleStr) {
+			continue
+		}
+		line = line[len(moduleStr):]
+		n := len(line)
+		line = bytes.TrimSpace(line)
+		if len(line) == n || len(line) == 0 {
+			continue
+		}
+
+		if line[0] == '"' || line[0] == '`' {
+			p, err := strconv.Unquote(string(line))
+			if err != nil {
+				return "" // malformed quoted string or multiline module path
+			}
+			return p
+		}
+
+		return string(line)
+	}
+	return "" // missing module path
+}
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
new file mode 100644
index 00000000..c602667d
--- /dev/null
+++ b/x/mod/modfile/rule.go
@@ -0,0 +1,1299 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package modfile
+
+import (
+	"errors"
+	"fmt"
+	"path/filepath"
+	"sort"
+	"strconv"
+	"strings"
+	"unicode"
+
+	"golang.org/x/mod/module"
+	"golang.org/x/mod/semver"
+
+	"github.com/goplus/gop/x/mod/internal/lazyregexp"
+)
+
+// A File is the parsed, interpreted form of a gop.mod file.
+type File struct {
+	Module    *Module
+	Go        *Go
+	Gop       *Gop
+	Classfile *Classfile
+	Register  *Register
+	Require   []*Require
+	Exclude   []*Exclude
+	Replace   []*Replace
+	Retract   []*Retract
+
+	Syntax *FileSyntax
+}
+
+// A Module is the module statement.
+type Module struct {
+	Mod    module.Version
+	Syntax *Line
+}
+
+// A Go is the go statement.
+type Go struct {
+	Version string // "1.23"
+	Syntax  *Line
+}
+
+// A Gop is the gop statement.
+type Gop struct {
+	Version string // "1.23"
+	Syntax  *Line
+}
+
+// A Classfile is the classfile statement.
+type Classfile struct {
+	ProjExt  string   // ".gmx"
+	WorkExt  string   // ".spx"
+	PkgPaths []string // paths of classfile
+	Syntax   *Line
+}
+
+// A Register is the register statement.
+type Register struct {
+	ClassfileMod string // modname of classfile
+	Syntax       *Line
+}
+
+// A Require is a single require statement.
+type Require struct {
+	Mod      module.Version
+	Indirect bool // has "// indirect" comment
+	Syntax   *Line
+}
+
+// An Exclude is a single exclude statement.
+type Exclude struct {
+	Mod    module.Version
+	Syntax *Line
+}
+
+// A Replace is a single replace statement.
+type Replace struct {
+	Old    module.Version
+	New    module.Version
+	Syntax *Line
+}
+
+// A Retract is a single retract statement.
+type Retract struct {
+	VersionInterval
+	Rationale string
+	Syntax    *Line
+}
+
+// A VersionInterval represents a range of versions with upper and lower bounds.
+// Intervals are closed: both bounds are included. When Low is equal to High,
+// the interval may refer to a single version ('v1.2.3') or an interval
+// ('[v1.2.3, v1.2.3]'); both have the same representation.
+type VersionInterval struct {
+	Low, High string
+}
+
+func (f *File) AddModuleStmt(path string) error {
+	if f.Syntax == nil {
+		f.Syntax = new(FileSyntax)
+	}
+	if f.Module == nil {
+		f.Module = &Module{
+			Mod:    module.Version{Path: path},
+			Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)),
+		}
+	} else {
+		f.Module.Mod.Path = path
+		f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path))
+	}
+	return nil
+}
+
+func (f *File) AddComment(text string) {
+	if f.Syntax == nil {
+		f.Syntax = new(FileSyntax)
+	}
+	f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{
+		Comments: Comments{
+			Before: []Comment{
+				{
+					Token: text,
+				},
+			},
+		},
+	})
+}
+
+type VersionFixer func(path, version string) (string, error)
+
+// errDontFix is returned by a VersionFixer to indicate the version should be
+// left alone, even if it's not canonical.
+var dontFixRetract VersionFixer = func(_, vers string) (string, error) {
+	return vers, nil
+}
+
+// Parse parses the data, reported in errors as being from file,
+// into a File struct. It applies fix, if non-nil, to canonicalize all module versions found.
+func Parse(file string, data []byte, fix VersionFixer) (*File, error) {
+	return parseToFile(file, data, fix, true)
+}
+
+// ParseLax is like Parse but ignores unknown statements.
+// It is used when parsing gop.mod files other than the main module,
+// under the theory that most statement types we add in the future will
+// only apply in the main module, like exclude and replace,
+// and so we get better gradual deployments if old go commands
+// simply ignore those statements when found in gop.mod files
+// in dependencies.
+func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) {
+	return parseToFile(file, data, fix, false)
+}
+
+func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) {
+	fs, err := parse(file, data)
+	if err != nil {
+		return nil, err
+	}
+	f := &File{
+		Syntax: fs,
+	}
+	var errs ErrorList
+
+	// fix versions in retract directives after the file is parsed.
+	// We need the module path to fix versions, and it might be at the end.
+	defer func() {
+		oldLen := len(errs)
+		f.fixRetract(fix, &errs)
+		if len(errs) > oldLen {
+			parsed, err = nil, errs
+		}
+	}()
+
+	for _, x := range fs.Stmt {
+		switch x := x.(type) {
+		case *Line:
+			f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict)
+
+		case *LineBlock:
+			if len(x.Token) > 1 {
+				if strict {
+					errs = append(errs, Error{
+						Filename: file,
+						Pos:      x.Start,
+						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
+					})
+				}
+				continue
+			}
+			switch x.Token[0] {
+			default:
+				if strict {
+					errs = append(errs, Error{
+						Filename: file,
+						Pos:      x.Start,
+						Err:      fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
+					})
+				}
+				continue
+			case "module", "require", "exclude", "replace", "retract":
+				for _, l := range x.Line {
+					f.add(&errs, x, l, x.Token[0], l.Token, fix, strict)
+				}
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return nil, errs
+	}
+	return f, nil
+}
+
+var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+var GopVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+
+func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
+	// If strict is false, this module is a dependency.
+	// We ignore all unknown directives as well as main-module-only
+	// directives like replace and exclude. It will work better for
+	// forward compatibility if we can depend on modules that have unknown
+	// statements (presumed relevant only when acting as the main module)
+	// and simply ignore those statements.
+	if !strict {
+		switch verb {
+		case "go", "gop", "classfile", "register", "module", "retract", "require":
+			// want these even for dependency gop.mods
+		default:
+			return
+		}
+	}
+
+	wrapModPathError := func(modPath string, err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			ModPath:  modPath,
+			Verb:     verb,
+			Err:      err,
+		})
+	}
+	wrapError := func(err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			Err:      err,
+		})
+	}
+	errorf := func(format string, args ...interface{}) {
+		wrapError(fmt.Errorf(format, args...))
+	}
+
+	switch verb {
+	default:
+		errorf("unknown directive: %s", verb)
+
+	case "go":
+		if f.Go != nil {
+			errorf("repeated go statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("go directive expects exactly one argument")
+			return
+		} else if !GoVersionRE.MatchString(args[0]) {
+			errorf("invalid go version '%s': must match format 1.23", args[0])
+			return
+		}
+
+		f.Go = &Go{Syntax: line}
+		f.Go.Version = args[0]
+
+	case "gop":
+		if f.Gop != nil {
+			errorf("repeated gop statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("gop directive expects exactly one argument")
+			return
+		} else if !GopVersionRE.MatchString(args[0]) {
+			errorf("invalid go version '%s': must match format 1.23", args[0])
+			return
+		}
+
+		f.Gop = &Gop{Syntax: line}
+		f.Gop.Version = args[0]
+
+	case "classfile":
+		if len(args) <= 2 {
+			errorf("usage: %s [classfileExt...] pkgPathOfClassFile", verb)
+			return
+		}
+		f.Classfile = &Classfile{Syntax: line}
+
+		projExt, err := parseExt(&args[0])
+		if err != nil {
+			errorf("invalid extension: %v", err)
+			return
+		}
+
+		workExt, err := parseExt(&args[1])
+		if err != nil {
+			errorf("invalid extension: %v", err)
+			return
+		}
+		f.Classfile.ProjExt = projExt
+		f.Classfile.WorkExt = workExt
+
+		for i := 2; i < len(args); i++ {
+			s, err := parseString(&args[i])
+			if err != nil {
+				errorf("invalid quoted string: %v", err)
+				return
+			}
+			f.Classfile.PkgPaths = append(f.Classfile.PkgPaths, s)
+		}
+
+	case "register":
+		s, err := parseString(&args[len(args)-1])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		err = module.CheckPath(s)
+		if err != nil {
+			wrapError(err)
+			return
+		}
+		f.Register = &Register{
+			ClassfileMod: s,
+			Syntax:       line,
+		}
+
+	case "module":
+		if f.Module != nil {
+			errorf("repeated module statement")
+			return
+		}
+		f.Module = &Module{Syntax: line}
+		if len(args) != 1 {
+			errorf("usage: module module/path")
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		f.Module.Mod = module.Version{Path: s}
+
+	case "require", "exclude":
+		if len(args) != 2 {
+			errorf("usage: %s module/path v1.2.3", verb)
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		v, err := parseVersion(verb, s, &args[1], fix)
+		if err != nil {
+			wrapError(err)
+			return
+		}
+		pathMajor, err := modulePathMajor(s)
+		if err != nil {
+			wrapError(err)
+			return
+		}
+		if err := module.CheckPathMajor(v, pathMajor); err != nil {
+			wrapModPathError(s, err)
+			return
+		}
+		if verb == "require" {
+			f.Require = append(f.Require, &Require{
+				Mod:      module.Version{Path: s, Version: v},
+				Syntax:   line,
+				Indirect: isIndirect(line),
+			})
+		} else {
+			f.Exclude = append(f.Exclude, &Exclude{
+				Mod:    module.Version{Path: s, Version: v},
+				Syntax: line,
+			})
+		}
+
+	case "replace":
+		arrow := 2
+		if len(args) >= 2 && args[1] == "=>" {
+			arrow = 1
+		}
+		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
+			errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		pathMajor, err := modulePathMajor(s)
+		if err != nil {
+			wrapModPathError(s, err)
+			return
+		}
+		var v string
+		if arrow == 2 {
+			v, err = parseVersion(verb, s, &args[1], fix)
+			if err != nil {
+				wrapError(err)
+				return
+			}
+			if err := module.CheckPathMajor(v, pathMajor); err != nil {
+				wrapModPathError(s, err)
+				return
+			}
+		}
+		ns, err := parseString(&args[arrow+1])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		nv := ""
+		if len(args) == arrow+2 {
+			if !IsDirectoryPath(ns) {
+				errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
+				return
+			}
+			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
+				errorf("replacement directory appears to be Windows path (on a non-windows system)")
+				return
+			}
+		}
+		if len(args) == arrow+3 {
+			nv, err = parseVersion(verb, ns, &args[arrow+2], fix)
+			if err != nil {
+				wrapError(err)
+				return
+			}
+			if IsDirectoryPath(ns) {
+				errorf("replacement module directory path %q cannot have version", ns)
+				return
+			}
+		}
+		f.Replace = append(f.Replace, &Replace{
+			Old:    module.Version{Path: s, Version: v},
+			New:    module.Version{Path: ns, Version: nv},
+			Syntax: line,
+		})
+
+	case "retract":
+		rationale := parseRetractRationale(block, line)
+		vi, err := parseVersionInterval(verb, "", &args, dontFixRetract)
+		if err != nil {
+			if strict {
+				wrapError(err)
+				return
+			} else {
+				// Only report errors parsing intervals in the main module. We may
+				// support additional syntax in the future, such as open and half-open
+				// intervals. Those can't be supported now, because they break the
+				// gop.mod parser, even in lax mode.
+				return
+			}
+		}
+		if len(args) > 0 && strict {
+			// In the future, there may be additional information after the version.
+			errorf("unexpected token after version: %q", args[0])
+			return
+		}
+		retract := &Retract{
+			VersionInterval: vi,
+			Rationale:       rationale,
+			Syntax:          line,
+		}
+		f.Retract = append(f.Retract, retract)
+	}
+}
+
+// fixRetract applies fix to each retract directive in f, appending any errors
+// to errs.
+//
+// Most versions are fixed as we parse the file, but for retract directives,
+// the relevant module path is the one specified with the module directive,
+// and that might appear at the end of the file (or not at all).
+func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) {
+	if fix == nil {
+		return
+	}
+	path := ""
+	if f.Module != nil {
+		path = f.Module.Mod.Path
+	}
+	var r *Retract
+	wrapError := func(err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      r.Syntax.Start,
+			Err:      err,
+		})
+	}
+
+	for _, r = range f.Retract {
+		if path == "" {
+			wrapError(errors.New("no module directive found, so retract cannot be used"))
+			return // only print the first one of these
+		}
+
+		args := r.Syntax.Token
+		if args[0] == "retract" {
+			args = args[1:]
+		}
+		vi, err := parseVersionInterval("retract", path, &args, fix)
+		if err != nil {
+			wrapError(err)
+		}
+		r.VersionInterval = vi
+	}
+}
+
+// isIndirect reports whether line has a "// indirect" comment,
+// meaning it is in gop.mod only for its effect on indirect dependencies,
+// so that it can be dropped entirely once the effective version of the
+// indirect dependency reaches the given minimum version.
+func isIndirect(line *Line) bool {
+	if len(line.Suffix) == 0 {
+		return false
+	}
+	f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash)))
+	return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;")
+}
+
+// setIndirect sets line to have (or not have) a "// indirect" comment.
+func setIndirect(line *Line, indirect bool) {
+	if isIndirect(line) == indirect {
+		return
+	}
+	if indirect {
+		// Adding comment.
+		if len(line.Suffix) == 0 {
+			// New comment.
+			line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
+			return
+		}
+
+		com := &line.Suffix[0]
+		text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash)))
+		if text == "" {
+			// Empty comment.
+			com.Token = "// indirect"
+			return
+		}
+
+		// Insert at beginning of existing comment.
+		com.Token = "// indirect; " + text
+		return
+	}
+
+	// Removing comment.
+	f := strings.Fields(line.Suffix[0].Token)
+	if len(f) == 2 {
+		// Remove whole comment.
+		line.Suffix = nil
+		return
+	}
+
+	// Remove comment prefix.
+	com := &line.Suffix[0]
+	i := strings.Index(com.Token, "indirect;")
+	com.Token = "//" + com.Token[i+len("indirect;"):]
+}
+
+// IsDirectoryPath reports whether the given path should be interpreted
+// as a directory path. Just like on the go command line, relative paths
+// and rooted paths are directory paths; the rest are module paths.
+func IsDirectoryPath(ns string) bool {
+	// Because gop.mod files can move from one system to another,
+	// we check all known path syntaxes, both Unix and Windows.
+	return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") ||
+		strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) ||
+		len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':'
+}
+
+// MustQuote reports whether s must be quoted in order to appear as
+// a single token in a gop.mod line.
+func MustQuote(s string) bool {
+	for _, r := range s {
+		switch r {
+		case ' ', '"', '\'', '`':
+			return true
+
+		case '(', ')', '[', ']', '{', '}', ',':
+			if len(s) > 1 {
+				return true
+			}
+
+		default:
+			if !unicode.IsPrint(r) {
+				return true
+			}
+		}
+	}
+	return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*")
+}
+
+// AutoQuote returns s or, if quoting is required for s to appear in a gop.mod,
+// the quotation of s.
+func AutoQuote(s string) string {
+	if MustQuote(s) {
+		return strconv.Quote(s)
+	}
+	return s
+}
+
+func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) {
+	toks := *args
+	if len(toks) == 0 || toks[0] == "(" {
+		return VersionInterval{}, fmt.Errorf("expected '[' or version")
+	}
+	if toks[0] != "[" {
+		v, err := parseVersion(verb, path, &toks[0], fix)
+		if err != nil {
+			return VersionInterval{}, err
+		}
+		*args = toks[1:]
+		return VersionInterval{Low: v, High: v}, nil
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 {
+		return VersionInterval{}, fmt.Errorf("expected version after '['")
+	}
+	low, err := parseVersion(verb, path, &toks[0], fix)
+	if err != nil {
+		return VersionInterval{}, err
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 || toks[0] != "," {
+		return VersionInterval{}, fmt.Errorf("expected ',' after version")
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 {
+		return VersionInterval{}, fmt.Errorf("expected version after ','")
+	}
+	high, err := parseVersion(verb, path, &toks[0], fix)
+	if err != nil {
+		return VersionInterval{}, err
+	}
+	toks = toks[1:]
+
+	if len(toks) == 0 || toks[0] != "]" {
+		return VersionInterval{}, fmt.Errorf("expected ']' after version")
+	}
+	toks = toks[1:]
+
+	*args = toks
+	return VersionInterval{Low: low, High: high}, nil
+}
+
+func parseString(s *string) (string, error) {
+	t := *s
+	if strings.HasPrefix(t, `"`) {
+		var err error
+		if t, err = strconv.Unquote(t); err != nil {
+			return "", err
+		}
+	} else if strings.ContainsAny(t, "\"'`") {
+		// Other quotes are reserved both for possible future expansion
+		// and to avoid confusion. For example if someone types 'x'
+		// we want that to be a syntax error and not a literal x in literal quotation marks.
+		return "", fmt.Errorf("unquoted string cannot contain quote")
+	}
+	*s = AutoQuote(t)
+	return t, nil
+}
+
+// parseRetractRationale extracts the rationale for a retract directive from the
+// surrounding comments. If the line does not have comments and is part of a
+// block that does have comments, the block's comments are used.
+func parseRetractRationale(block *LineBlock, line *Line) string {
+	comments := line.Comment()
+	if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 {
+		comments = block.Comment()
+	}
+	groups := [][]Comment{comments.Before, comments.Suffix}
+	var lines []string
+	for _, g := range groups {
+		for _, c := range g {
+			if !strings.HasPrefix(c.Token, "//") {
+				continue // blank line
+			}
+			lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//")))
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+type ErrorList []Error
+
+func (e ErrorList) Error() string {
+	errStrs := make([]string, len(e))
+	for i, err := range e {
+		errStrs[i] = err.Error()
+	}
+	return strings.Join(errStrs, "\n")
+}
+
+type Error struct {
+	Filename string
+	Pos      Position
+	Verb     string
+	ModPath  string
+	Err      error
+}
+
+func (e *Error) Error() string {
+	var pos string
+	if e.Pos.LineRune > 1 {
+		// Don't print LineRune if it's 1 (beginning of line).
+		// It's always 1 except in scanner errors, which are rare.
+		pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune)
+	} else if e.Pos.Line > 0 {
+		pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line)
+	} else if e.Filename != "" {
+		pos = fmt.Sprintf("%s: ", e.Filename)
+	}
+
+	var directive string
+	if e.ModPath != "" {
+		directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath)
+	} else if e.Verb != "" {
+		directive = fmt.Sprintf("%s: ", e.Verb)
+	}
+
+	return pos + directive + e.Err.Error()
+}
+
+func (e *Error) Unwrap() error { return e.Err }
+
+func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) {
+	t, err := parseString(s)
+	if err != nil {
+		return "", &Error{
+			Verb:    verb,
+			ModPath: path,
+			Err: &module.InvalidVersionError{
+				Version: *s,
+				Err:     err,
+			},
+		}
+	}
+	if fix != nil {
+		fixed, err := fix(path, t)
+		if err != nil {
+			if err, ok := err.(*module.ModuleError); ok {
+				return "", &Error{
+					Verb:    verb,
+					ModPath: path,
+					Err:     err.Err,
+				}
+			}
+			return "", err
+		}
+		t = fixed
+	} else {
+		cv := module.CanonicalVersion(t)
+		if cv == "" {
+			return "", &Error{
+				Verb:    verb,
+				ModPath: path,
+				Err: &module.InvalidVersionError{
+					Version: t,
+					Err:     errors.New("must be of the form v1.2.3"),
+				},
+			}
+		}
+		t = cv
+	}
+	*s = t
+	return *s, nil
+}
+
+func parseExt(s *string) (string, error) {
+	t, err := parseString(s)
+	if err != nil {
+		return "", &InvalidExtError{
+			Ext: t,
+			Err: err,
+		}
+	}
+	if len(t) < 2 {
+		return "", &InvalidExtError{
+			Ext: *s,
+			Err: fmt.Errorf("invalid ext format"),
+		}
+	}
+	if t[0] != '.' {
+		return "", &InvalidExtError{
+			Ext: *s,
+			Err: fmt.Errorf("invalid ext format"),
+		}
+	}
+	*s = t
+	return *s, nil
+}
+
+func modulePathMajor(path string) (string, error) {
+	_, major, ok := module.SplitPathVersion(path)
+	if !ok {
+		return "", fmt.Errorf("invalid module path")
+	}
+	return major, nil
+}
+
+func (f *File) Format() ([]byte, error) {
+	return Format(f.Syntax), nil
+}
+
+// Cleanup cleans up the file f after any edit operations.
+// To avoid quadratic behavior, modifications like DropRequire
+// clear the entry but do not remove it from the slice.
+// Cleanup cleans out all the cleared entries.
+func (f *File) Cleanup() {
+	w := 0
+	for _, r := range f.Require {
+		if r.Mod.Path != "" {
+			f.Require[w] = r
+			w++
+		}
+	}
+	f.Require = f.Require[:w]
+
+	w = 0
+	for _, x := range f.Exclude {
+		if x.Mod.Path != "" {
+			f.Exclude[w] = x
+			w++
+		}
+	}
+	f.Exclude = f.Exclude[:w]
+
+	w = 0
+	for _, r := range f.Replace {
+		if r.Old.Path != "" {
+			f.Replace[w] = r
+			w++
+		}
+	}
+	f.Replace = f.Replace[:w]
+
+	w = 0
+	for _, r := range f.Retract {
+		if r.Low != "" || r.High != "" {
+			f.Retract[w] = r
+			w++
+		}
+	}
+	f.Retract = f.Retract[:w]
+
+	f.Syntax.Cleanup()
+}
+
+func (f *File) AddGoStmt(version string) error {
+	if !GoVersionRE.MatchString(version) {
+		return fmt.Errorf("invalid language version string %q", version)
+	}
+	if f.Go == nil {
+		var hint Expr
+		if f.Module != nil && f.Module.Syntax != nil {
+			hint = f.Module.Syntax
+		}
+		f.Go = &Go{
+			Version: version,
+			Syntax:  f.Syntax.addLine(hint, "go", version),
+		}
+	} else {
+		f.Go.Version = version
+		f.Syntax.updateLine(f.Go.Syntax, "go", version)
+	}
+	return nil
+}
+
+func (f *File) AddRequire(path, vers string) error {
+	need := true
+	for _, r := range f.Require {
+		if r.Mod.Path == path {
+			if need {
+				r.Mod.Version = vers
+				f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers)
+				need = false
+			} else {
+				f.Syntax.removeLine(r.Syntax)
+				*r = Require{}
+			}
+		}
+	}
+
+	if need {
+		f.AddNewRequire(path, vers, false)
+	}
+	return nil
+}
+
+func (f *File) AddNewRequire(path, vers string, indirect bool) {
+	line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
+	setIndirect(line, indirect)
+	f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
+}
+
+func (f *File) SetRequire(req []*Require) {
+	need := make(map[string]string)
+	indirect := make(map[string]bool)
+	for _, r := range req {
+		need[r.Mod.Path] = r.Mod.Version
+		indirect[r.Mod.Path] = r.Indirect
+	}
+
+	for _, r := range f.Require {
+		if v, ok := need[r.Mod.Path]; ok {
+			r.Mod.Version = v
+			r.Indirect = indirect[r.Mod.Path]
+		} else {
+			*r = Require{}
+		}
+	}
+
+	var newStmts []Expr
+	for _, stmt := range f.Syntax.Stmt {
+		switch stmt := stmt.(type) {
+		case *LineBlock:
+			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
+				var newLines []*Line
+				for _, line := range stmt.Line {
+					if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
+						if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 {
+							line.Comments.Before = line.Comments.Before[:0]
+						}
+						line.Token[1] = need[p]
+						delete(need, p)
+						setIndirect(line, indirect[p])
+						newLines = append(newLines, line)
+					}
+				}
+				if len(newLines) == 0 {
+					continue // drop stmt
+				}
+				stmt.Line = newLines
+			}
+
+		case *Line:
+			if len(stmt.Token) > 0 && stmt.Token[0] == "require" {
+				if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
+					stmt.Token[2] = need[p]
+					delete(need, p)
+					setIndirect(stmt, indirect[p])
+				} else {
+					continue // drop stmt
+				}
+			}
+		}
+		newStmts = append(newStmts, stmt)
+	}
+	f.Syntax.Stmt = newStmts
+
+	for path, vers := range need {
+		f.AddNewRequire(path, vers, indirect[path])
+	}
+	f.SortBlocks()
+}
+
+func (f *File) DropRequire(path string) error {
+	for _, r := range f.Require {
+		if r.Mod.Path == path {
+			f.Syntax.removeLine(r.Syntax)
+			*r = Require{}
+		}
+	}
+	return nil
+}
+
+// AddExclude adds a exclude statement to the mod file. Errors if the provided
+// version is not a canonical version string
+func (f *File) AddExclude(path, vers string) error {
+	if err := checkCanonicalVersion(path, vers); err != nil {
+		return err
+	}
+
+	var hint *Line
+	for _, x := range f.Exclude {
+		if x.Mod.Path == path && x.Mod.Version == vers {
+			return nil
+		}
+		if x.Mod.Path == path {
+			hint = x.Syntax
+		}
+	}
+
+	f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)})
+	return nil
+}
+
+func (f *File) DropExclude(path, vers string) error {
+	for _, x := range f.Exclude {
+		if x.Mod.Path == path && x.Mod.Version == vers {
+			f.Syntax.removeLine(x.Syntax)
+			*x = Exclude{}
+		}
+	}
+	return nil
+}
+
+func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error {
+	need := true
+	old := module.Version{Path: oldPath, Version: oldVers}
+	new := module.Version{Path: newPath, Version: newVers}
+	tokens := []string{"replace", AutoQuote(oldPath)}
+	if oldVers != "" {
+		tokens = append(tokens, oldVers)
+	}
+	tokens = append(tokens, "=>", AutoQuote(newPath))
+	if newVers != "" {
+		tokens = append(tokens, newVers)
+	}
+
+	var hint *Line
+	for _, r := range f.Replace {
+		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
+			if need {
+				// Found replacement for old; update to use new.
+				r.New = new
+				f.Syntax.updateLine(r.Syntax, tokens...)
+				need = false
+				continue
+			}
+			// Already added; delete other replacements for same.
+			f.Syntax.removeLine(r.Syntax)
+			*r = Replace{}
+		}
+		if r.Old.Path == oldPath {
+			hint = r.Syntax
+		}
+	}
+	if need {
+		f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)})
+	}
+	return nil
+}
+
+func (f *File) DropReplace(oldPath, oldVers string) error {
+	for _, r := range f.Replace {
+		if r.Old.Path == oldPath && r.Old.Version == oldVers {
+			f.Syntax.removeLine(r.Syntax)
+			*r = Replace{}
+		}
+	}
+	return nil
+}
+
+// AddRetract adds a retract statement to the mod file. Errors if the provided
+// version interval does not consist of canonical version strings
+func (f *File) AddRetract(vi VersionInterval, rationale string) error {
+	var path string
+	if f.Module != nil {
+		path = f.Module.Mod.Path
+	}
+	if err := checkCanonicalVersion(path, vi.High); err != nil {
+		return err
+	}
+	if err := checkCanonicalVersion(path, vi.Low); err != nil {
+		return err
+	}
+
+	r := &Retract{
+		VersionInterval: vi,
+	}
+	if vi.Low == vi.High {
+		r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low))
+	} else {
+		r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
+	}
+	if rationale != "" {
+		for _, line := range strings.Split(rationale, "\n") {
+			com := Comment{Token: "// " + line}
+			r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
+		}
+	}
+	return nil
+}
+
+func (f *File) DropRetract(vi VersionInterval) error {
+	for _, r := range f.Retract {
+		if r.VersionInterval == vi {
+			f.Syntax.removeLine(r.Syntax)
+			*r = Retract{}
+		}
+	}
+	return nil
+}
+
+func (f *File) SortBlocks() {
+	f.removeDups() // otherwise sorting is unsafe
+
+	for _, stmt := range f.Syntax.Stmt {
+		block, ok := stmt.(*LineBlock)
+		if !ok {
+			continue
+		}
+		less := lineLess
+		if block.Token[0] == "retract" {
+			less = lineRetractLess
+		}
+		sort.SliceStable(block.Line, func(i, j int) bool {
+			return less(block.Line[i], block.Line[j])
+		})
+	}
+}
+
+// removeDups removes duplicate exclude and replace directives.
+//
+// Earlier exclude directives take priority.
+//
+// Later replace directives take priority.
+//
+// require directives are not de-duplicated. That's left up to higher-level
+// logic (MVS).
+//
+// retract directives are not de-duplicated since comments are
+// meaningful, and versions may be retracted multiple times.
+func (f *File) removeDups() {
+	kill := make(map[*Line]bool)
+
+	// Remove duplicate excludes.
+	haveExclude := make(map[module.Version]bool)
+	for _, x := range f.Exclude {
+		if haveExclude[x.Mod] {
+			kill[x.Syntax] = true
+			continue
+		}
+		haveExclude[x.Mod] = true
+	}
+	var excl []*Exclude
+	for _, x := range f.Exclude {
+		if !kill[x.Syntax] {
+			excl = append(excl, x)
+		}
+	}
+	f.Exclude = excl
+
+	// Remove duplicate replacements.
+	// Later replacements take priority over earlier ones.
+	haveReplace := make(map[module.Version]bool)
+	for i := len(f.Replace) - 1; i >= 0; i-- {
+		x := f.Replace[i]
+		if haveReplace[x.Old] {
+			kill[x.Syntax] = true
+			continue
+		}
+		haveReplace[x.Old] = true
+	}
+	var repl []*Replace
+	for _, x := range f.Replace {
+		if !kill[x.Syntax] {
+			repl = append(repl, x)
+		}
+	}
+	f.Replace = repl
+
+	// Duplicate require and retract directives are not removed.
+
+	// Drop killed statements from the syntax tree.
+	var stmts []Expr
+	for _, stmt := range f.Syntax.Stmt {
+		switch stmt := stmt.(type) {
+		case *Line:
+			if kill[stmt] {
+				continue
+			}
+		case *LineBlock:
+			var lines []*Line
+			for _, line := range stmt.Line {
+				if !kill[line] {
+					lines = append(lines, line)
+				}
+			}
+			stmt.Line = lines
+			if len(lines) == 0 {
+				continue
+			}
+		}
+		stmts = append(stmts, stmt)
+	}
+	f.Syntax.Stmt = stmts
+}
+
+// lineLess returns whether li should be sorted before lj. It sorts
+// lexicographically without assigning any special meaning to tokens.
+func lineLess(li, lj *Line) bool {
+	for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
+		if li.Token[k] != lj.Token[k] {
+			return li.Token[k] < lj.Token[k]
+		}
+	}
+	return len(li.Token) < len(lj.Token)
+}
+
+// lineRetractLess returns whether li should be sorted before lj for lines in
+// a "retract" block. It treats each line as a version interval. Single versions
+// are compared as if they were intervals with the same low and high version.
+// Intervals are sorted in descending order, first by low version, then by
+// high version, using semver.Compare.
+func lineRetractLess(li, lj *Line) bool {
+	interval := func(l *Line) VersionInterval {
+		if len(l.Token) == 1 {
+			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
+		} else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" {
+			return VersionInterval{Low: l.Token[1], High: l.Token[3]}
+		} else {
+			// Line in unknown format. Treat as an invalid version.
+			return VersionInterval{}
+		}
+	}
+	vii := interval(li)
+	vij := interval(lj)
+	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
+		return cmp > 0
+	}
+	return semver.Compare(vii.High, vij.High) > 0
+}
+
+// checkCanonicalVersion returns a non-nil error if vers is not a canonical
+// version string or does not match the major version of path.
+//
+// If path is non-empty, the error text suggests a format with a major version
+// corresponding to the path.
+func checkCanonicalVersion(path, vers string) error {
+	_, pathMajor, pathMajorOk := module.SplitPathVersion(path)
+
+	if vers == "" || vers != module.CanonicalVersion(vers) {
+		if pathMajor == "" {
+			return &module.InvalidVersionError{
+				Version: vers,
+				Err:     fmt.Errorf("must be of the form v1.2.3"),
+			}
+		}
+		return &module.InvalidVersionError{
+			Version: vers,
+			Err:     fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)),
+		}
+	}
+
+	if pathMajorOk {
+		if err := module.CheckPathMajor(vers, pathMajor); err != nil {
+			if pathMajor == "" {
+				// In this context, the user probably wrote "v2.3.4" when they meant
+				// "v2.3.4+incompatible". Suggest that instead of "v0 or v1".
+				return &module.InvalidVersionError{
+					Version: vers,
+					Err:     fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)),
+				}
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
+type InvalidExtError struct {
+	Ext string
+	Err error
+}
+
+func (e *InvalidExtError) Error() string {
+	return fmt.Sprintf("ext %q invalid: %s", e.Ext, e.Err)
+}
+
+func (e *InvalidExtError) Unwrap() error { return e.Err }


Test patch:
diff --git a/x/mod/modfile/rule_test.go b/x/mod/modfile/rule_test.go
new file mode 100644
index 00000000..8ee6017b
--- /dev/null
+++ b/x/mod/modfile/rule_test.go
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package modfile
+
+import (
+	"testing"
+)
+
+var gopmod = `
+module spx
+
+go 1.17
+gop 1.1
+
+classfile .gmx .spx github.com/goplus/spx math
+
+require (
+    github.com/ajstarks/svgo v0.0.0-20210927141636-6d70534b1098
+)
+`
+
+var gopmod2 = `
+module moduleUserProj
+
+go 1.17
+gop 1.1
+
+register github.com/goplus/spx
+
+require (
+    github.com/goplus/spx v1.0
+)
+`
+
+func TestParse(t *testing.T) {
+	f, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod), func(path, vers string) (resolved string, err error) {
+		return vers, nil
+	})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if f.Gop.Version != "1.1" {
+		t.Errorf("gop version expected be 1.1, but %s got", f.Gop.Version)
+	}
+
+	if f.Classfile.ProjExt != ".gmx" {
+		t.Errorf("classfile exts expected be .gmx, but %s got", f.Classfile.ProjExt)
+	}
+	if f.Classfile.WorkExt != ".spx" {
+		t.Errorf("classfile exts expected be .spx, but %s got", f.Classfile.WorkExt)
+	}
+
+	if len(f.Classfile.PkgPaths) != 2 {
+		t.Errorf("classfile pkgpaths length expected be 2, but %d got", len(f.Classfile.PkgPaths))
+	}
+
+	if f.Classfile.PkgPaths[0] != "github.com/goplus/spx" {
+		t.Errorf("classfile path expected be github.com/goplus/spx, but %s got", f.Classfile.PkgPaths[0])
+	}
+	if f.Classfile.PkgPaths[1] != "math" {
+		t.Errorf("classfile path expected be math, but %s got", f.Classfile.PkgPaths[1])
+	}
+
+	f2, err := Parse("github.com/goplus/gop/gop.mod", []byte(gopmod2), func(path, vers string) (resolved string, err error) {
+		return vers, nil
+	})
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if f2.Register.ClassfileMod != "github.com/goplus/spx" {
+		t.Errorf("register classfile mod expected be github.com/goplus/spx, but %s got", f2.Register.ClassfileMod)
+	}
+}