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 }