github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/repl/repl.go (about)

     1  package repl
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/format"
     9  	"go/parser"
    10  	"go/printer"
    11  	"go/token"
    12  	"io"
    13  	"text/template"
    14  
    15  	gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
    16  	"github.com/gnolang/gno/gnovm/tests"
    17  )
    18  
    19  const (
    20  	executedFunc = "main"
    21  	fileTemplate = `// generated by 'gno repl'
    22  // FILE NAME: test{{$.ID}}.gno
    23  package test
    24  {{range .Imports}}
    25  {{.}}
    26  {{end}}
    27  {{range .Declarations}}
    28  {{.}}
    29  {{end}}
    30  {{with .Expression}}
    31  // main function
    32  func main{{$.ID}}() {
    33  	{{.}}
    34  }
    35  {{end}}
    36  `
    37  )
    38  
    39  type tempModel struct {
    40  	ID           int
    41  	Declarations []string
    42  	Imports      []string
    43  	Expression   string
    44  }
    45  
    46  type state struct {
    47  	// imports contains all the imports added. they will be added to the following generated source code.
    48  	imports map[string]string
    49  
    50  	// files are all generated files. This is used when source code is requested.
    51  	files map[string]string
    52  
    53  	// id is the actual execution number. Used to avoid duplicated functions and file names.
    54  	id int
    55  
    56  	fileset *token.FileSet
    57  	machine *gno.Machine
    58  }
    59  
    60  func newState(stdout io.Writer, sf func() gno.Store) *state {
    61  	s := &state{
    62  		imports: make(map[string]string),
    63  		files:   make(map[string]string),
    64  		fileset: token.NewFileSet(),
    65  	}
    66  
    67  	s.machine = gno.NewMachineWithOptions(gno.MachineOptions{
    68  		PkgPath: "test",
    69  		Output:  stdout,
    70  		Store:   sf(),
    71  	})
    72  
    73  	return s
    74  }
    75  
    76  type ReplOption func(*Repl)
    77  
    78  // WithStore allows to modify the default Store implementation used by the VM.
    79  // If nil is provided, the VM will use a default implementation.
    80  func WithStore(s gno.Store) ReplOption {
    81  	return func(r *Repl) {
    82  		r.storeFunc = func() gno.Store {
    83  			return s
    84  		}
    85  	}
    86  }
    87  
    88  // WithStd changes std's reader and writers implementations. An internal bytes.Buffer is used by default.
    89  func WithStd(stdin io.Reader, stdout, stderr io.Writer) ReplOption {
    90  	return func(r *Repl) {
    91  		r.stdin = stdin
    92  		r.stdout = stdout
    93  		r.stderr = stderr
    94  	}
    95  }
    96  
    97  type Repl struct {
    98  	state *state
    99  	tmpl  *template.Template
   100  
   101  	// rw joins stdout and stderr to give an unified output and group with stdin.
   102  	rw bufio.ReadWriter
   103  
   104  	// Repl options:
   105  	storeFunc func() gno.Store
   106  	stdout    io.Writer
   107  	stderr    io.Writer
   108  	stdin     io.Reader
   109  }
   110  
   111  // NewRepl creates a Repl struct. It is able to process input source code and eventually run it.
   112  func NewRepl(opts ...ReplOption) *Repl {
   113  	t := template.Must(template.New("tmpl").Parse(fileTemplate))
   114  
   115  	r := &Repl{
   116  		tmpl: t,
   117  	}
   118  
   119  	var b bytes.Buffer
   120  	r.stdin = &b
   121  	r.stdout = &b
   122  	r.stderr = &b
   123  
   124  	r.storeFunc = func() gno.Store {
   125  		return tests.TestStore("teststore", "", r.stdin, r.stdout, r.stderr, tests.ImportModeStdlibsOnly)
   126  	}
   127  
   128  	for _, o := range opts {
   129  		o(r)
   130  	}
   131  
   132  	r.state = newState(r.stdout, r.storeFunc)
   133  
   134  	br := bufio.NewReader(r.stdin)
   135  	bw := bufio.NewWriter(io.MultiWriter(r.stderr, r.stdout))
   136  	r.rw = *bufio.NewReadWriter(br, bw)
   137  
   138  	return r
   139  }
   140  
   141  // Process accepts any valid Gno source code and executes it if it
   142  // is an expression, or stores it for later use if they are declarations.
   143  // If the provided input is not valid Gno source code, an error is returned.
   144  // If the execution on the VM is not successful, the panic is recovered and
   145  // returned as an error.
   146  func (r *Repl) Process(input string) (out string, err error) {
   147  	defer func() {
   148  		if r := recover(); r != nil {
   149  			err = fmt.Errorf("recovered from panic: %q", r)
   150  		}
   151  	}()
   152  	r.state.id++
   153  
   154  	decl, declErr := r.parseDeclaration(input)
   155  	if declErr == nil {
   156  		return r.handleDeclarations(decl)
   157  	}
   158  
   159  	exp, expErr := r.parseExpression(input)
   160  	if expErr == nil {
   161  		return r.handleExpression(exp)
   162  	}
   163  
   164  	return "", fmt.Errorf("error parsing code:\n\t- as expression: %w\n\t- as declarations: %w", expErr, declErr)
   165  }
   166  
   167  func (r *Repl) handleExpression(e *ast.File) (string, error) {
   168  	fn := r.filename()
   169  	src := r.nodeToString(e)
   170  
   171  	n := gno.MustParseFile(fn, src)
   172  	r.state.files[fn] = src
   173  	r.state.machine.RunFiles(n)
   174  	r.state.machine.RunStatement(gno.S(gno.Call(gno.X(fmt.Sprintf("%s%d", executedFunc, r.state.id)))))
   175  
   176  	// Read the result from the output buffer after calling main function.
   177  	b, err := io.ReadAll(r.rw)
   178  	if err != nil {
   179  		return "", fmt.Errorf("error reading output buffer: %w", err)
   180  	}
   181  
   182  	return string(b), nil
   183  }
   184  
   185  func (r *Repl) handleDeclarations(fn *ast.File) (string, error) {
   186  	var (
   187  		b               bytes.Buffer
   188  		nonImportsCount int
   189  	)
   190  	ast.Inspect(fn, func(n ast.Node) bool {
   191  		var (
   192  			writeNode bool
   193  			ns        string
   194  		)
   195  
   196  		switch t := n.(type) {
   197  		case *ast.GenDecl:
   198  			tok := t.Tok
   199  
   200  			if tok != token.IMPORT &&
   201  				tok != token.TYPE &&
   202  				tok != token.CONST &&
   203  				tok != token.VAR {
   204  				break
   205  			}
   206  
   207  			writeNode = true
   208  			ns = r.nodeToString(n)
   209  
   210  			if tok != token.IMPORT {
   211  				nonImportsCount++
   212  				break
   213  			}
   214  
   215  			i, ok := t.Specs[0].(*ast.ImportSpec)
   216  			if !ok {
   217  				break
   218  			}
   219  
   220  			r.state.imports[i.Path.Value] = ns
   221  		case *ast.FuncDecl:
   222  			writeNode = true
   223  			nonImportsCount++
   224  			ns = r.nodeToString(n)
   225  		}
   226  
   227  		if !writeNode {
   228  			return true
   229  		}
   230  
   231  		b.WriteString(ns)
   232  		b.WriteByte('\n')
   233  
   234  		return false
   235  	})
   236  
   237  	// Avoid adding files with only imports on it.
   238  	if nonImportsCount != 0 {
   239  		name := r.filename()
   240  		src := r.nodeToString(fn)
   241  
   242  		n := gno.MustParseFile(name, src)
   243  		r.state.files[name] = src
   244  		r.state.machine.RunFiles(n)
   245  	}
   246  
   247  	return b.String(), nil
   248  }
   249  
   250  func (r *Repl) nodeToString(n ast.Node) string {
   251  	var b bytes.Buffer
   252  	if err := printer.Fprint(&b, r.state.fileset, n); err != nil {
   253  		panic(err)
   254  	}
   255  	return b.String()
   256  }
   257  
   258  func (r *Repl) filename() string {
   259  	return fmt.Sprintf("test%d.gno", r.state.id)
   260  }
   261  
   262  func (r *Repl) parseExpression(input string) (*ast.File, error) {
   263  	return r.executeAndParse(&tempModel{
   264  		Expression: input,
   265  	})
   266  }
   267  
   268  func (r *Repl) parseDeclaration(input string) (*ast.File, error) {
   269  	return r.executeAndParse(&tempModel{
   270  		Declarations: []string{input},
   271  	})
   272  }
   273  
   274  func (r *Repl) executeAndParse(m *tempModel) (*ast.File, error) {
   275  	var b bytes.Buffer
   276  
   277  	// Set the id to the template.
   278  	m.ID = r.state.id
   279  
   280  	// Add all the imports to all the files, they will be removed after formatting.
   281  	for _, e := range r.state.imports {
   282  		m.Imports = append(m.Imports, e)
   283  	}
   284  
   285  	err := r.tmpl.Execute(&b, m)
   286  	if err != nil {
   287  		return nil, fmt.Errorf("error executing the template: %w", err)
   288  	}
   289  
   290  	// Format the source code to remove unused imports and improve code readability.
   291  	formatted, err := format.Source(b.Bytes())
   292  	if err != nil {
   293  		return nil, fmt.Errorf("error formatting code: %w", err)
   294  	}
   295  
   296  	name := r.filename()
   297  	return parser.ParseFile(r.state.fileset, name, formatted, parser.AllErrors|parser.ParseComments)
   298  }
   299  
   300  // Reset will reset the actual repl state, restarting the internal VM.
   301  func (r *Repl) Reset() {
   302  	r.state.machine.Release()
   303  	r.state = newState(r.stdout, r.storeFunc)
   304  }
   305  
   306  const separator = "//----------------------------------//\n"
   307  
   308  // Src will print all the valid code introduced on this Repl session.
   309  func (r *Repl) Src() string {
   310  	var b bytes.Buffer
   311  
   312  	b.WriteString(separator)
   313  	for _, s := range r.state.files {
   314  		b.WriteString(s)
   315  		b.WriteString(separator)
   316  	}
   317  
   318  	return b.String()
   319  }