github.com/goplus/igop@v0.17.0/repl.go (about)

     1  /*
     2   * Copyright (c) 2022 The GoPlus Authors (goplus.org). All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package igop
    18  
    19  import (
    20  	"fmt"
    21  	"go/ast"
    22  	"go/constant"
    23  	"go/scanner"
    24  	"go/token"
    25  	"go/types"
    26  	"runtime"
    27  	"strings"
    28  
    29  	xconst "github.com/goplus/igop/constant"
    30  	"golang.org/x/tools/go/ssa"
    31  )
    32  
    33  /*
    34  package main
    35  
    36  import "fmt"
    37  
    38  const/var/type/func
    39  
    40  func main(){}
    41  */
    42  
    43  type Repl struct {
    44  	ctx       *Context               // interp context
    45  	pkg       *ssa.Package           // main package
    46  	builtin   *ast.File              // builtin func
    47  	interp    *Interp                // last interp
    48  	fsInit    *fnState               // func init
    49  	fsMain    *fnState               // func main
    50  	globalMap map[string]interface{} // keep repl global map
    51  	fileName  string                 // file.go or file.gop
    52  	source    string                 // all source
    53  	imports   []string               // import lines
    54  	globals   []string               // global var/func/type
    55  	infuncs   []string               // in main func
    56  	lastDump  []string               // last __igop_repl_dump__
    57  }
    58  
    59  func toDump(i []interface{}) (dump []string) {
    60  	for _, v := range i {
    61  		dump = append(dump, fmt.Sprintf("%v %T", v, v))
    62  	}
    63  	return
    64  }
    65  
    66  func toResultDump(interp *Interp, vs []interface{}, rs *types.Tuple) (dump []string) {
    67  	for i, v := range vs {
    68  		dump = append(dump, fmt.Sprintf("%v %v", v, interp.toType(rs.At(i).Type())))
    69  	}
    70  	return
    71  }
    72  
    73  func NewRepl(ctx *Context) *Repl {
    74  	r := &Repl{
    75  		ctx:       ctx,
    76  		globalMap: make(map[string]interface{}),
    77  	}
    78  	ctx.SetEvalMode(true)
    79  	RegisterCustomBuiltin("__igop_repl_used__", func(v interface{}) {})
    80  	RegisterCustomBuiltin("__igop_repl_dump__", func(v ...interface{}) {
    81  		r.lastDump = toDump(v)
    82  	})
    83  	// reset runtime.GC to default
    84  	ctx.RegisterExternal("runtime.GC", runtime.GC)
    85  	ctx.evalCallFn = func(interp *Interp, call *ssa.Call, res ...interface{}) {
    86  		if len(*call.Referrers()) != 0 {
    87  			return
    88  		}
    89  		v := call.Call.Value
    90  		if un, ok := v.(*ssa.UnOp); ok {
    91  			v = un.X
    92  		}
    93  		if strings.Contains(v.Name(), "__igop_repl_") {
    94  			return
    95  		}
    96  		r.lastDump = toResultDump(interp, res, call.Call.Signature().Results())
    97  	}
    98  	if f, err := ParseBuiltin(r.ctx.FileSet, "main"); err == nil {
    99  		r.builtin = f
   100  	}
   101  	return r
   102  }
   103  
   104  func (r *Repl) SetFileName(filename string) {
   105  	r.fileName = filename
   106  }
   107  
   108  func (r *Repl) Eval(expr string) (tok token.Token, dump []string, err error) {
   109  	r.lastDump = nil
   110  	tok = r.firstToken(expr)
   111  	err = r.eval(tok, expr)
   112  	return tok, r.lastDump, err
   113  }
   114  
   115  func (r *Repl) Interp() *Interp {
   116  	return r.interp
   117  }
   118  
   119  func (r *Repl) Source() string {
   120  	return r.source
   121  }
   122  
   123  func (r *Repl) buildSource(expr string, tok token.Token) string {
   124  	v0 := strings.Join(r.imports, "\n")
   125  	v1 := strings.Join(r.globals, "\n")
   126  	v2 := strings.Join(r.infuncs, "\n")
   127  	switch tok {
   128  	case token.IMPORT:
   129  		v0 += "\n" + expr
   130  	case token.CONST, token.VAR, token.TYPE, token.FUNC:
   131  		v1 += "\n" + expr
   132  	default:
   133  		v2 += "\n" + expr
   134  	}
   135  	return fmt.Sprintf(`package main
   136  %v
   137  %v
   138  func main() {
   139  	%v
   140  }
   141  `, v0, v1, v2)
   142  }
   143  
   144  func (r *Repl) eval(tok token.Token, expr string) (err error) {
   145  	var src string
   146  	var inMain bool
   147  	var evalConst bool
   148  	switch tok {
   149  	case token.PACKAGE:
   150  		// skip package
   151  		return nil
   152  	case token.IMPORT:
   153  		src = r.buildSource(expr, tok)
   154  		errors, err := r.check(r.fileName, src)
   155  		if err != nil {
   156  			return err
   157  		}
   158  		if errors != nil {
   159  			return errors[0]
   160  		}
   161  		r.imports = append(r.imports, expr)
   162  		return nil
   163  	case token.FUNC:
   164  		src = r.buildSource(expr, tok)
   165  		errors, err := r.check(r.fileName, src)
   166  		if err != nil {
   167  			// check funlit
   168  			msg := err.Error()
   169  			if strings.Contains(msg, errMaybeGoFunLit) || strings.Contains(msg, errMaybeGopFunLit) {
   170  				inMain = true
   171  				src = r.buildSource(expr, token.ILLEGAL)
   172  				errors, err = r.check(r.fileName, src)
   173  			}
   174  			if err != nil {
   175  				return err
   176  			}
   177  		}
   178  		if errors != nil {
   179  			return errors[0]
   180  		}
   181  	case token.CONST, token.TYPE, token.VAR:
   182  		src = r.buildSource(expr, tok)
   183  		errors, err := r.check(r.fileName, src)
   184  		if err != nil {
   185  			return err
   186  		}
   187  		if errors != nil {
   188  			return errors[0]
   189  		}
   190  	default:
   191  		switch tok {
   192  		case token.FOR, token.IF, token.SWITCH, token.SELECT:
   193  			expr = "func(){\n" + expr + "\n}()"
   194  		}
   195  		inMain = true
   196  		src = r.buildSource(expr, tok)
   197  		errors, err := r.check(r.fileName, src)
   198  		if err != nil {
   199  			return err
   200  		}
   201  		var fixed []string
   202  		for _, err := range errors {
   203  			e, ok := err.(types.Error)
   204  			if !ok {
   205  				return e
   206  			}
   207  			if strings.HasSuffix(e.Msg, errDeclaredNotUsed) {
   208  				v := e.Msg[0 : len(e.Msg)-len(errDeclaredNotUsed)-1]
   209  				fixed = append(fixed, "__igop_repl_used__(&"+v+")")
   210  				// fixed = append(fixed, "__igop_repl_dump__("+v+")")
   211  			} else if strings.HasSuffix(e.Msg, errIsNotUsed) {
   212  				if _, ok := extractConstant([]byte(e.Msg[:len(e.Msg)-len(errIsNotUsed)])); ok {
   213  					// r.lastDump = []string{c.Lit}
   214  					// return nil
   215  					expr = "const __igop_repl_const__ = " + expr
   216  					tok = token.CONST
   217  					evalConst = true
   218  				} else {
   219  					expr = "__igop_repl_dump__(" + expr + ")"
   220  				}
   221  			} else {
   222  				return e
   223  			}
   224  		}
   225  		if len(fixed) != 0 {
   226  			expr += "\n" + strings.Join(fixed, "\n")
   227  		}
   228  		src = r.buildSource(expr, tok)
   229  	}
   230  	r.pkg, err = r.ctx.LoadFile(r.fileName, src)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	i, err := newInterp(r.ctx, r.pkg, r.globalMap)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	rinit, err := r.runFunc(i, "init", r.fsInit)
   239  	if err == nil {
   240  		rinit.pc--
   241  	}
   242  	rmain, err := r.runFunc(i, "main", r.fsMain)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	if evalConst {
   247  		m := i.mainpkg.Members["__igop_repl_const__"]
   248  		c, _ := m.(*ssa.NamedConst)
   249  		s, _ := xconst.ExactConstantEx(c.Value.Value, true)
   250  		kind := c.Value.Value.Kind()
   251  		if kind == constant.Float {
   252  			es := c.Value.Value.ExactString()
   253  			r.lastDump = []string{fmt.Sprintf("%v %v\n(%v)", s, c.Type(), es)}
   254  		} else {
   255  			r.lastDump = []string{fmt.Sprintf("%v %v", s, c.Type())}
   256  		}
   257  		return nil
   258  	}
   259  	r.interp = i
   260  	r.fsInit = rinit
   261  	r.fsMain = rmain
   262  	for k, v := range i.globals {
   263  		r.globalMap[k] = v
   264  	}
   265  	if inMain {
   266  		r.infuncs = append(r.infuncs, expr)
   267  	} else {
   268  		r.globals = append(r.globals, expr)
   269  	}
   270  	r.source = src
   271  	return nil
   272  }
   273  
   274  const (
   275  	errIsNotUsed      = "is not used"
   276  	errMaybeGoFunLit  = `expected 'IDENT', found '{'`
   277  	errMaybeGopFunLit = `expected '(',`
   278  )
   279  
   280  func (r *Repl) check(filename string, src interface{}) (errors []error, err error) {
   281  	file, err := r.ctx.ParseFile(filename, src)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	tc := &types.Config{
   286  		Importer:                 NewImporter(r.ctx),
   287  		DisableUnusedImportCheck: true,
   288  	}
   289  	tc.Error = func(err error) {
   290  		errors = append(errors, err)
   291  	}
   292  	pkg := types.NewPackage(file.Name.Name, "")
   293  	var files []*ast.File
   294  	if r.builtin != nil {
   295  		files = []*ast.File{r.builtin, file}
   296  	} else {
   297  		files = []*ast.File{file}
   298  	}
   299  	types.NewChecker(tc, r.ctx.FileSet, pkg, &types.Info{}).Files(files)
   300  	return
   301  }
   302  
   303  func (r *Repl) firstToken(src string) token.Token {
   304  	var s scanner.Scanner
   305  	file := r.ctx.FileSet.AddFile("", -1, len(src))
   306  	s.Init(file, []byte(src), nil, 0)
   307  	_, tok, _ := s.Scan()
   308  	return tok
   309  }
   310  
   311  type fnState struct {
   312  	fr *frame
   313  	pc int
   314  }
   315  
   316  func (r *Repl) runFunc(i *Interp, fnname string, fs *fnState) (rfs *fnState, err error) {
   317  	defer func() {
   318  		switch p := recover().(type) {
   319  		case nil:
   320  		case exitPanic:
   321  			err = ExitError(int(p))
   322  		default:
   323  			err = fmt.Errorf("%v", p)
   324  		}
   325  	}()
   326  	fn := r.pkg.Func(fnname)
   327  	if fn == nil {
   328  		return nil, fmt.Errorf("no function %v", fnname)
   329  	}
   330  	pfn := i.funcs[fn]
   331  	fr := pfn.allocFrame(&frame{})
   332  	if fs != nil {
   333  		copy(fr.stack, fs.fr.stack)
   334  		fr.ipc = fs.pc
   335  	}
   336  	var pc int
   337  	for fr.ipc != -1 {
   338  		fn := fr.pfn.Instrs[fr.ipc]
   339  		pc = fr.ipc
   340  		fr.ipc++
   341  		fn(fr)
   342  	}
   343  	return &fnState{fr: fr, pc: pc}, nil
   344  }
   345  
   346  type tokenLit struct {
   347  	Lit string
   348  	Pos token.Pos
   349  	Tok token.Token
   350  }
   351  
   352  // extractConstant extract constant int and overflows float/complex
   353  func extractConstant(src []byte) (constant *tokenLit, ok bool) {
   354  	var s scanner.Scanner
   355  	fset := token.NewFileSet()
   356  	file := fset.AddFile("", fset.Base(), len(src))
   357  	s.Init(file, src, nil, 0)
   358  	pos, tok, lit := s.Scan()
   359  	if tok == token.EOF {
   360  		return
   361  	}
   362  	first := &tokenLit{lit, pos, tok}
   363  	var check int
   364  	for {
   365  		_, tok, lit := s.Scan()
   366  		if tok == token.EOF {
   367  			break
   368  		}
   369  		switch tok {
   370  		case token.LPAREN:
   371  			check++
   372  		case token.RPAREN:
   373  			check--
   374  		case token.IDENT:
   375  			if check == 1 && lit == "constant" {
   376  				var nodes []*tokenLit
   377  			loop:
   378  				for {
   379  					pos, tok, lit := s.Scan()
   380  					if tok == token.EOF {
   381  						break
   382  					}
   383  					switch tok {
   384  					case token.LPAREN:
   385  						check++
   386  					case token.RPAREN:
   387  						check--
   388  						if check == 0 {
   389  							break loop
   390  						}
   391  					default:
   392  						nodes = append(nodes, &tokenLit{lit, pos, tok})
   393  					}
   394  				}
   395  				switch len(nodes) {
   396  				case 0:
   397  					return first, true
   398  				case 1:
   399  					switch nodes[0].Tok {
   400  					case token.INT:
   401  						return nodes[0], true
   402  					case token.FLOAT:
   403  						return nodes[0], true
   404  						// extract if not parse float64
   405  						// if _, err := strconv.ParseFloat(nodes[0].Lit, 128); err != nil {
   406  						// 	return nodes[0], true
   407  						// }
   408  					}
   409  				default:
   410  					last := nodes[len(nodes)-1]
   411  					return &tokenLit{string(src[int(nodes[0].Pos)-1 : int(last.Pos)+len(last.Lit)]), nodes[0].Pos, token.IMAG}, true
   412  				}
   413  			}
   414  		}
   415  	}
   416  	return
   417  }