├── .gitignore ├── README.markdown └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | repl 3 | .*.swp 4 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | A compiling Go REPL. 2 | 3 | Builds up Go source as the session goes on, compiles and runs it with every input. 4 | 5 | A "!" in front of the input means it's in "unstable" mode, e.g. a package has been imported and isn't used, or errors occur in the source. 6 | 7 | Example session: 8 | 9 | Welcome to the Go REPL! 10 | Enter '?' for a list of commands. 11 | > ? 12 | Commands: 13 | ? help 14 | + (pkg) import package 15 | - (pkg) remove package 16 | -[dpc] pop last (declaration|package|code) 17 | ~ reset 18 | : (...) add persistent code 19 | ! inspect source 20 | > a := 6 21 | > b := 7 22 | > println(a * b) 23 | 42 24 | > + fmt 25 | ! fmt> fmt.Println("Hello, world!") 26 | Hello, world! 27 | ! fmt> println("This won't work since fmt doesn't get used.") 28 | Compile error: /tmp/gorepl.go:2: imported and not used: fmt 29 | 30 | ! fmt> : fmt.Print() 31 | fmt> println("Now it will!") 32 | Now it will! 33 | fmt> func b(a interface{}) { fmt.Printf("You passed: %#v\n", a); } 34 | fmt> b(1) 35 | Compile error: /tmp/gorepl.go:14: cannot call non-function b (type int) 36 | 37 | fmt> ! 38 | package main 39 | import "fmt" 40 | 41 | func b(a interface{}) { fmt.Printf("You passed: %#v\n", a) } 42 | 43 | func noop(_ interface{}) {} 44 | 45 | func main() { 46 | a := 6; 47 | noop(a); 48 | b := 7; 49 | noop(b); 50 | fmt.Print(); 51 | } 52 | 53 | fmt> -d 54 | fmt> ! 55 | package main 56 | import "fmt" 57 | 58 | func noop(_ interface{}) {} 59 | 60 | func main() { 61 | a := 6; 62 | noop(a); 63 | b := 7; 64 | noop(b); 65 | fmt.Print(); 66 | } 67 | 68 | fmt> func dump(a interface{}) { fmt.Printf("You passed: %#v\n", a); } 69 | fmt> dump("Phew, there we go.") 70 | You passed: "Phew, there we go." 71 | fmt> -d 72 | fmt> -c 73 | ! fmt> - fmt 74 | > + math 75 | ! math> println(math.Pi) 76 | +3.141593e+000 77 | ! math> + fmt 78 | ! math fmt> fmt.Println(math.Pi) 79 | 3.1415927 80 | ! math fmt> 81 | 82 | TODO: Write automatic test with the above example session as input and expected output. 83 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "bufio" 6 | "bytes" 7 | "fmt" 8 | "go/ast" 9 | "go/parser" 10 | "go/token" 11 | "go/printer" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "strings" 16 | "strconv" 17 | ) 18 | 19 | type World struct { 20 | pkgs *[]string 21 | defs *[]string 22 | code *[]interface{} 23 | files *token.FileSet 24 | exec string 25 | unstable bool 26 | write_src_mode bool 27 | } 28 | 29 | const TEMPPATH = "/tmp/gorepl" 30 | 31 | 32 | var ( 33 | bin = os.Getenv("GOBIN") 34 | arch = map[string]string{ 35 | "amd64": "6", 36 | "386": "8", 37 | "arm": "5", 38 | }[os.Getenv("GOARCH")] 39 | ) 40 | 41 | func indentCode(text string, indent string) string { 42 | return strings.Join(strings.Split(text, "\n"), "\n"+indent) 43 | } 44 | 45 | func (self *World) source() string { 46 | return self.source_print(false) 47 | } 48 | 49 | func (self *World) source_print(print_linenums bool) string { 50 | source := "package main\n" 51 | 52 | pkgs_num := 0 53 | defs_num := 0 54 | code_num := 0 55 | if print_linenums { source = "\n " + source } 56 | 57 | for _, v := range *self.pkgs { 58 | if print_linenums { 59 | source += "p"+strconv.Itoa(pkgs_num)+": " 60 | pkgs_num += 1 61 | } 62 | source += "import \"" + v + "\"\n" 63 | } 64 | 65 | source += "\n" 66 | 67 | for _, d := range *self.defs { 68 | if print_linenums { 69 | source += "d"+strconv.Itoa(defs_num)+": " 70 | defs_num += 1 71 | } 72 | source += indentCode(d, " ") + "\n\n" 73 | } 74 | 75 | if print_linenums { source += " " } 76 | source += "func noop(_ interface{}) {}\n\n" 77 | if print_linenums { source += " " } 78 | source += "func main() {\n" 79 | 80 | for _, c := range *self.code { 81 | str := new(bytes.Buffer) 82 | printer.Fprint(str, self.files, c) 83 | 84 | if print_linenums { 85 | source += "c"+strconv.Itoa(code_num)+": " 86 | code_num += 1 87 | } 88 | source += "\t" + indentCode(str.String(), "\t") + ";\n" 89 | switch c.(type) { 90 | case *ast.AssignStmt: 91 | for _, name := range c.(*ast.AssignStmt).Lhs { 92 | str := new(bytes.Buffer) 93 | printer.Fprint(str, self.files, name) 94 | source += "\t" + "noop(" + str.String() + ");\n" 95 | } 96 | } 97 | } 98 | 99 | if self.exec != "" { 100 | source += "\t" + self.exec + ";\n" 101 | } 102 | 103 | if print_linenums { source += " " } 104 | source += "}\n" 105 | 106 | return source 107 | } 108 | 109 | func compile(w *World) *bytes.Buffer { 110 | ioutil.WriteFile(TEMPPATH+".go", []byte(w.source()), 0644) 111 | 112 | errBuf := new(bytes.Buffer) 113 | 114 | if arch == "" { 115 | arch = "6" // Most likely 64-bit architecture 116 | } 117 | cmd := exec.Command("echo", "") 118 | 119 | if bin != "" { 120 | cmd = exec.Command(bin+"/"+arch+"g", 121 | "-o", TEMPPATH + "."+arch, TEMPPATH + ".go") 122 | } else { 123 | cmd = exec.Command("go", "tool", arch+"g", 124 | "-o", TEMPPATH + "."+arch, TEMPPATH + ".go") 125 | } 126 | cmdout,err := cmd.StdoutPipe() 127 | err = cmd.Start() 128 | if err != nil { 129 | panic(err) 130 | } 131 | io.Copy(errBuf, cmdout) 132 | err = cmd.Wait() 133 | if errBuf.Len() > 0 { 134 | return errBuf 135 | } 136 | 137 | if bin != "" { 138 | cmd = exec.Command(bin+"/"+arch+"l", 139 | "-o", TEMPPATH, TEMPPATH + "."+arch) 140 | } else { 141 | cmd = exec.Command("go", "tool", arch+"l", 142 | "-o", TEMPPATH, TEMPPATH + "."+arch) 143 | } 144 | cmdout,err = cmd.StdoutPipe() 145 | 146 | err = cmd.Start() 147 | if err != nil { 148 | panic(err) 149 | } 150 | io.Copy(errBuf, cmdout) 151 | err = cmd.Wait() 152 | 153 | return errBuf 154 | } 155 | 156 | func run() (*bytes.Buffer, *bytes.Buffer) { 157 | outBuf := new(bytes.Buffer) 158 | errBuf := new(bytes.Buffer) 159 | 160 | cmd := exec.Command( 161 | TEMPPATH,TEMPPATH) 162 | 163 | stdout,err := cmd.StdoutPipe() 164 | if err != nil { 165 | panic(err) 166 | } 167 | 168 | stderr,err := cmd.StderrPipe() 169 | if err != nil { 170 | panic(err) 171 | } 172 | err = cmd.Start() 173 | if err != nil { 174 | panic(err) 175 | } 176 | io.Copy(errBuf, stderr) 177 | 178 | if errBuf.Len() > 0 { 179 | return nil, errBuf 180 | } 181 | 182 | io.Copy(outBuf, stdout) 183 | 184 | return outBuf, errBuf 185 | } 186 | 187 | func intf2str(src interface{}) string { 188 | switch s := src.(type) { 189 | case string: 190 | return s 191 | } 192 | return "" 193 | } 194 | 195 | func ParseStmtList(fset *token.FileSet, filename string, src interface{}) ([]ast.Stmt, error) { 196 | //f, err := parser.ParseFile(fset, filename, src, 0) 197 | f, err := parser.ParseFile(fset, filename, "package p;func _(){"+intf2str(src)+"\n}", 0) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return f.Decls[0].(*ast.FuncDecl).Body.List, nil 202 | } 203 | 204 | func ParseDeclList(fset *token.FileSet, filename string, src interface{}) ([]ast.Decl, error) { 205 | //f, err := parser.ParseFile(fset, filename, src, 0) 206 | f, err := parser.ParseFile(fset, filename, "package p;"+intf2str(src), 0) 207 | if err != nil { 208 | return nil, err 209 | } 210 | return f.Decls, nil 211 | } 212 | 213 | func exec_check_alias(line string) string { 214 | if line == "help" { 215 | return "?" 216 | } else if strings.HasPrefix(line, "import") { 217 | return strings.Replace(line, "import", "+", 1) 218 | } else if line == "reset" { 219 | return "~" 220 | } else if line == "source" { 221 | return "!" 222 | } 223 | return line 224 | } 225 | 226 | func exec_special(w *World, line string) bool { 227 | if line == "auto" { // For autosetup 228 | *w.pkgs = append(*w.pkgs, "fmt") 229 | *w.pkgs = append(*w.pkgs, "math") 230 | *w.pkgs = append(*w.pkgs, "strings") 231 | *w.pkgs = append(*w.pkgs, "strconv") 232 | *w.defs = append(*w.defs, "var __Print_n, __Print_err = fmt.Print(\"\")") 233 | *w.defs = append(*w.defs, "var __Pi = math.Pi") 234 | *w.defs = append(*w.defs, "var __Trim_Nil = strings.Trim(\" \", \" \")") 235 | *w.defs = append(*w.defs, "var __Num_Itoa = strconv.Itoa(5)") 236 | *w.defs = append(*w.defs, "var print = fmt.Println") 237 | *w.defs = append(*w.defs, "var printf = fmt.Printf") 238 | w.unstable = compile(w).Len() > 0 239 | return true 240 | } 241 | if line == "run" { // For running without a command 242 | if err := compile(w); err.Len() > 0 { 243 | fmt.Println("Compile error:", err) 244 | } else if out, err := run(); err.Len() > 0 { 245 | fmt.Println("Runtime error:\n", err) 246 | } else { 247 | fmt.Print(out) 248 | } 249 | return true 250 | } 251 | if line == "write" { // For writing to source only 252 | w.write_src_mode = true 253 | return true 254 | } 255 | if line == "repl" { // For running in repl mode (by default) 256 | w.write_src_mode = false 257 | return true 258 | } 259 | return false 260 | } 261 | 262 | 263 | 264 | 265 | 266 | 267 | // Code Removal 268 | func cmd_remove_by_index(w *World, cmd_args string) { 269 | if len(cmd_args) == 0 { 270 | fmt.Println("Fatal error: cmd_args is empty") 271 | return 272 | } 273 | 274 | item_type := cmd_args[0] 275 | item_list_len := map[uint8]int{ 276 | 'd': len(*w.defs)+1, 277 | 'p': len(*w.pkgs)+1, 278 | 'c': len(*w.code)+1, 279 | }[item_type] - 1 280 | item_name := map[uint8]string { 281 | 'd': "declarations", 282 | 'p': "packages", 283 | 'c': "code", 284 | }[item_type] 285 | 286 | if item_list_len == -1 { 287 | fmt.Printf("Remove: Invalid item type '%c'\n", item_type) 288 | return 289 | } 290 | if item_list_len == 0 { 291 | fmt.Printf("Remove: no more %s to remove\n", item_name) 292 | return 293 | } 294 | items_to_remove := cmd_remove_get_item_indices(item_list_len, cmd_args) 295 | 296 | switch item_type { 297 | case 'd': 298 | cmd_remove_declarations_by_index(w, items_to_remove) 299 | case 'p': 300 | cmd_remove_packages_by_index(w, items_to_remove) 301 | case 'c': 302 | cmd_remove_code_by_index(w, items_to_remove) 303 | default: 304 | fmt.Printf("Fatal error: Invalid item type '%c'\n", item_type) 305 | return 306 | } 307 | } 308 | 309 | func cmd_remove_get_item_indices(item_list_len int, cmd_args string) []bool { 310 | items_to_remove := make([]bool, item_list_len) 311 | 312 | if len(cmd_args) == 1 { 313 | items_to_remove[item_list_len - 1] = true 314 | return items_to_remove 315 | } 316 | 317 | item_indices := strings.Split(cmd_args[1:], ",") 318 | 319 | for _, item_index_str := range item_indices { 320 | if item_index_str == "" { continue } 321 | item_index, err := strconv.Atoi(item_index_str) 322 | if err != nil { 323 | fmt.Printf("Remove: %s not integer\n", item_index_str) 324 | continue 325 | } 326 | if item_index < 0 || item_index >= item_list_len { 327 | fmt.Printf("Remove: %d out of range\n", item_index) 328 | continue 329 | } 330 | if items_to_remove[item_index] { 331 | fmt.Printf("Remove: %d already in list\n", item_index) 332 | continue 333 | } 334 | items_to_remove[item_index] = true 335 | } 336 | 337 | return items_to_remove 338 | } 339 | 340 | // The unfortunate fact is that these three functions could not be merged, as 341 | // the w.code type is different from the other two (and is necessary, since 342 | // there is a need to keep track of if it is an assignment or not). 343 | // Since Go is a static type language (and the interface{} type cannot be 344 | // used here as intended), these three are left on their own. The rest has 345 | // already been abstracted above, thankfully. 346 | func cmd_remove_declarations_by_index(w *World, defs_to_remove []bool) { 347 | new_index := 0 348 | for old_index, def_item := range *w.defs { 349 | if defs_to_remove[old_index] { 350 | continue 351 | } 352 | (*w.defs)[new_index] = def_item 353 | new_index += 1 354 | } 355 | *w.defs = (*w.defs)[:new_index] 356 | } 357 | 358 | func cmd_remove_packages_by_index(w *World, pkgs_to_remove []bool) { 359 | new_index := 0 360 | for old_index, pkg_item := range *w.pkgs { 361 | if pkgs_to_remove[old_index] { 362 | continue 363 | } 364 | (*w.pkgs)[new_index] = pkg_item 365 | new_index += 1 366 | } 367 | *w.pkgs = (*w.pkgs)[:new_index] 368 | } 369 | 370 | func cmd_remove_code_by_index(w *World, code_to_remove []bool) { 371 | new_index := 0 372 | for old_index, code_item := range *w.code { 373 | if code_to_remove[old_index] { 374 | continue 375 | } 376 | (*w.code)[new_index] = code_item 377 | new_index += 1 378 | } 379 | *w.code = (*w.code)[:new_index] 380 | } 381 | 382 | func cmd_remove_packages_by_name(w *World, cmd_args string) { 383 | if len(cmd_args) == 0 { 384 | fmt.Println("Fatal error: cmd_args is empty") 385 | return 386 | } 387 | 388 | pkg_index_list := []string{} 389 | for _, pkg_name := range strings.Split(cmd_args, " ") { 390 | if pkg_name == "" { continue } 391 | pkg_index := -1 392 | for i, v := range *w.pkgs { 393 | if v == pkg_name { 394 | pkg_index = i 395 | break 396 | } 397 | } 398 | if pkg_index == -1 { 399 | fmt.Printf("Remove: No such package '%s'\n", pkg_name) 400 | continue 401 | } 402 | fmt.Printf("Remove: Removing '%s' at %d\n",pkg_name, pkg_index) 403 | pkg_index_list = append(pkg_index_list,strconv.Itoa(pkg_index)) 404 | } 405 | cmd_remove_by_index(w, "p"+strings.Join(pkg_index_list, ",")) 406 | } 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | func main() { 419 | fmt.Println("Welcome to the Go REPL!") 420 | fmt.Println("Enter '?' for a list of commands.") 421 | 422 | w := new(World) 423 | w.pkgs = &[]string{} 424 | w.code = &[]interface{}{} 425 | w.defs = &[]string{} 426 | w.files = token.NewFileSet() 427 | w.unstable = false 428 | w.write_src_mode = false 429 | 430 | buf := bufio.NewReader(os.Stdin) 431 | for { 432 | if w.unstable { 433 | fmt.Print("! ") 434 | } 435 | 436 | fmt.Print(strings.Join(*w.pkgs, " ") + "> ") 437 | 438 | read, err := buf.ReadString('\n') 439 | if err != nil { 440 | println() 441 | break 442 | } 443 | 444 | line := strings.Trim(read[0 : len(read)-1], " ") 445 | if len(line) == 0 { 446 | continue 447 | } 448 | line = exec_check_alias(line) 449 | if exec_special(w, line) { 450 | continue 451 | } 452 | 453 | w.exec = "" 454 | cmd_args := strings.Trim(line[1:]," ") 455 | 456 | switch line[0] { 457 | case '?': 458 | fmt.Println("Symbol Commands:") 459 | fmt.Println("\t? \thelp menu") 460 | fmt.Println("\t+ (pkg) (pkg) ...\timport package") 461 | fmt.Println("\t- (pkg) (pkg) ...\tremove package") 462 | fmt.Println("\t-[dpc][#],[#],...\tpop last/specific (declaration|package|code)") 463 | fmt.Println("\t~ \treset") 464 | fmt.Println("\t: (...) \tadd persistent code") 465 | fmt.Println("\t! \tinspect source") 466 | 467 | fmt.Println("Word Commands:") 468 | fmt.Println("\thelp \thelp menu") 469 | fmt.Println("\timport (pkg) (pkg) ...\timport package") 470 | fmt.Println("\treset \treset") 471 | fmt.Println("\tsource \tinspect source") 472 | fmt.Println("\trun \trun source") 473 | fmt.Println("\twrite \twrite source mode on") 474 | fmt.Println("\trepl \twrite source mode off") 475 | fmt.Println("\tauto \tautosetup with some standard packages") 476 | 477 | fmt.Println("For removal, -[dpc] is equivalent to -[dpc]") 478 | case '+': 479 | allpkgs := strings.Split(cmd_args, " ") 480 | fmt.Println("Importing: ") 481 | for _, pkg_name := range allpkgs { 482 | if pkg_name != "" { 483 | *w.pkgs = append(*w.pkgs, pkg_name) 484 | fmt.Println(" ", len(*w.pkgs), pkg_name) 485 | } 486 | } 487 | w.unstable = compile(w).Len() > 0 488 | case '-': 489 | if len(cmd_args) == 0 { 490 | fmt.Println("No item specified for removal.") 491 | } else if line[1] != ' ' { 492 | cmd_remove_by_index(w, cmd_args) 493 | } else { 494 | cmd_remove_packages_by_name(w, cmd_args) 495 | } 496 | w.unstable = compile(w).Len() > 0 497 | case '~': 498 | *w.pkgs = (*w.pkgs)[:0] 499 | *w.defs = (*w.defs)[:0] 500 | *w.code = (*w.code)[:0] 501 | w.unstable = false 502 | w.write_src_mode = false 503 | case '!': 504 | fmt.Println(w.source_print(true)) 505 | case ':': 506 | line = line + ";" 507 | tree, err := ParseStmtList(w.files, "go-repl", cmd_args) 508 | if err != nil { 509 | fmt.Println("Parse error:", err) 510 | continue 511 | } 512 | 513 | for _, v := range tree { 514 | *w.code = append(*w.code, v) 515 | } 516 | 517 | w.unstable = compile(w).Len() > 0 518 | default: 519 | line = line + ";" 520 | var tree interface{} 521 | tree, err := ParseDeclList(w.files, "go-repl", line[0:]) 522 | if err != nil { 523 | tree, err = ParseStmtList(w.files, "go-repl", line[0:]) 524 | if err != nil { 525 | fmt.Println("Parse error:", err) 526 | continue 527 | } else { 528 | fmt.Println("CODE: ", line[0:]) 529 | } 530 | } else { 531 | fmt.Println("DECL: ", line[0:]) 532 | } 533 | 534 | changed := false 535 | got_err := false 536 | bkup_pkgs := *w.pkgs 537 | bkup_code := *w.code 538 | bkup_defs := *w.defs 539 | bkup_files := w.files 540 | bkup_exec := w.exec 541 | 542 | switch tree.(type) { 543 | case []ast.Stmt: 544 | for _, v := range tree.([]ast.Stmt) { 545 | if w.write_src_mode { 546 | *w.code = append(*w.code, v) 547 | continue 548 | } 549 | str := new(bytes.Buffer) 550 | printer.Fprint(str, w.files, v) 551 | 552 | switch v.(type) { 553 | case *ast.AssignStmt: 554 | *w.code = append(*w.code,v) 555 | changed = true 556 | default: 557 | w.exec = str.String() 558 | } 559 | } 560 | case []ast.Decl: 561 | for _, v := range tree.([]ast.Decl) { 562 | str := new(bytes.Buffer) 563 | printer.Fprint(str, w.files, v) 564 | 565 | *w.defs = append(*w.defs,str.String()) 566 | } 567 | 568 | changed = true 569 | default: 570 | fmt.Println("Fatal error: Unknown tree type.") 571 | } 572 | 573 | if w.write_src_mode { continue } 574 | 575 | if err := compile(w); err.Len() > 0 { 576 | fmt.Println("Compile error:", err) 577 | got_err = true 578 | } else if out, err := run(); err.Len() > 0 { 579 | fmt.Println("Runtime error:\n", err) 580 | got_err = true 581 | } else { 582 | fmt.Print(out) 583 | } 584 | 585 | if got_err { 586 | *w.pkgs = bkup_pkgs 587 | *w.code = bkup_code 588 | *w.defs = bkup_defs 589 | w.files = bkup_files 590 | w.exec = bkup_exec 591 | continue 592 | } 593 | 594 | if changed && got_err { 595 | w.unstable = true 596 | fmt.Println("Fatal error: Code should not run") 597 | } 598 | 599 | if changed { 600 | w.unstable = compile(w).Len() > 0 601 | } 602 | } 603 | } 604 | } 605 | --------------------------------------------------------------------------------