interpret.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /*
  2. * ECAL
  3. *
  4. * Copyright 2020 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the MIT
  7. * License, If a copy of the MIT License was not distributed with this
  8. * file, You can obtain one at https://opensource.org/licenses/MIT.
  9. */
  10. package tool
  11. import (
  12. "bytes"
  13. "encoding/json"
  14. "flag"
  15. "fmt"
  16. "io"
  17. "io/ioutil"
  18. "os"
  19. "path/filepath"
  20. "runtime/pprof"
  21. "strings"
  22. "devt.de/krotik/common/errorutil"
  23. "devt.de/krotik/common/fileutil"
  24. "devt.de/krotik/common/stringutil"
  25. "devt.de/krotik/common/termutil"
  26. "devt.de/krotik/ecal/config"
  27. "devt.de/krotik/ecal/engine"
  28. "devt.de/krotik/ecal/interpreter"
  29. "devt.de/krotik/ecal/parser"
  30. "devt.de/krotik/ecal/scope"
  31. "devt.de/krotik/ecal/stdlib"
  32. "devt.de/krotik/ecal/util"
  33. )
  34. /*
  35. CLICustomHandler is a handler for custom operations.
  36. */
  37. type CLICustomHandler interface {
  38. CLIInputHandler
  39. /*
  40. LoadInitialFile clears the global scope and reloads the initial file.
  41. */
  42. LoadInitialFile(tid uint64) error
  43. }
  44. /*
  45. CLIInterpreter is a commandline interpreter for ECAL.
  46. */
  47. type CLIInterpreter struct {
  48. GlobalVS parser.Scope // Global variable scope
  49. RuntimeProvider *interpreter.ECALRuntimeProvider // Runtime provider of the interpreter
  50. // Customizations of output and input handling
  51. CustomHandler CLICustomHandler
  52. CustomWelcomeMessage string
  53. CustomHelpString string
  54. CustomRules []*engine.Rule
  55. EntryFile string // Entry file for the program
  56. LoadPlugins bool // Flag if stdlib plugins should be loaded
  57. // Parameter these can either be set programmatically or via CLI args
  58. Dir *string // Root dir for interpreter
  59. LogFile *string // Logfile (blank for stdout)
  60. LogLevel *string // Log level string (Debug, Info, Error)
  61. // User terminal
  62. Term termutil.ConsoleLineTerminal
  63. // Log output
  64. LogOut io.Writer
  65. }
  66. /*
  67. NewCLIInterpreter creates a new commandline interpreter for ECAL.
  68. */
  69. func NewCLIInterpreter() *CLIInterpreter {
  70. return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "",
  71. []*engine.Rule{}, "", true, nil, nil, nil, nil, os.Stdout}
  72. }
  73. /*
  74. ParseArgs parses the command line arguments. Call this after adding custon flags.
  75. Returns true if the program should exit.
  76. */
  77. func (i *CLIInterpreter) ParseArgs() bool {
  78. if i.Dir != nil && i.LogFile != nil && i.LogLevel != nil {
  79. return false
  80. }
  81. wd, _ := os.Getwd()
  82. i.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
  83. i.LogFile = flag.String("logfile", "", "Log to a file")
  84. i.LogLevel = flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
  85. showHelp := flag.Bool("help", false, "Show this help message")
  86. flag.Usage = func() {
  87. fmt.Fprintln(flag.CommandLine.Output())
  88. fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s run [options] [file]", osArgs[0]))
  89. fmt.Fprintln(flag.CommandLine.Output())
  90. flag.PrintDefaults()
  91. fmt.Fprintln(flag.CommandLine.Output())
  92. }
  93. if len(osArgs) >= 2 {
  94. flag.CommandLine.Parse(osArgs[2:])
  95. if cargs := flag.Args(); len(cargs) > 0 {
  96. i.EntryFile = flag.Arg(0)
  97. }
  98. if *showHelp {
  99. flag.Usage()
  100. }
  101. }
  102. return *showHelp
  103. }
  104. /*
  105. CreateRuntimeProvider creates the runtime provider of this interpreter. This function expects Dir,
  106. LogFile and LogLevel to be set.
  107. */
  108. func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
  109. var logger util.Logger
  110. var err error
  111. if i.RuntimeProvider != nil {
  112. return nil
  113. }
  114. // Check if we should log to a file
  115. if i.LogFile != nil && *i.LogFile != "" {
  116. var logWriter io.Writer
  117. logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
  118. logWriter, err = fileutil.NewMultiFileBuffer(*i.LogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
  119. logger = util.NewBufferLogger(logWriter)
  120. } else {
  121. // Log to the console by default
  122. logger = util.NewStdOutLogger()
  123. }
  124. // Set the log level
  125. if err == nil {
  126. if i.LogLevel != nil && *i.LogLevel != "" {
  127. logger, err = util.NewLogLevelLogger(logger, *i.LogLevel)
  128. }
  129. if err == nil {
  130. // Get the import locator
  131. importLocator := &util.FileImportLocator{Root: *i.Dir}
  132. // Create interpreter
  133. i.RuntimeProvider = interpreter.NewECALRuntimeProvider(name, importLocator, logger)
  134. }
  135. }
  136. return err
  137. }
  138. /*
  139. LoadInitialFile clears the global scope and reloads the initial file.
  140. */
  141. func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
  142. var err error
  143. i.RuntimeProvider.Processor.Finish()
  144. i.RuntimeProvider.Processor.Reset()
  145. // Add custom rules
  146. for _, r := range i.CustomRules {
  147. errorutil.AssertOk(i.RuntimeProvider.Processor.AddRule(r))
  148. }
  149. if i.CustomHandler != nil {
  150. i.CustomHandler.LoadInitialFile(tid)
  151. }
  152. i.GlobalVS.Clear()
  153. if i.EntryFile != "" {
  154. var ast *parser.ASTNode
  155. var initFile []byte
  156. initFile, err = ioutil.ReadFile(i.EntryFile)
  157. if err == nil {
  158. if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
  159. if err = ast.Runtime.Validate(); err == nil {
  160. _, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
  161. }
  162. defer func() {
  163. if i.RuntimeProvider.Debugger != nil {
  164. i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
  165. }
  166. }()
  167. }
  168. }
  169. }
  170. i.RuntimeProvider.Processor.Start()
  171. return err
  172. }
  173. /*
  174. CreateTerm creates a new console terminal for stdout.
  175. */
  176. func (i *CLIInterpreter) CreateTerm() error {
  177. var err error
  178. if i.Term == nil {
  179. i.Term, err = termutil.NewConsoleLineTerminal(os.Stdout)
  180. }
  181. return err
  182. }
  183. /*
  184. Interpret starts the ECAL code interpreter. Starts an interactive console in
  185. the current tty if the interactive flag is set.
  186. */
  187. func (i *CLIInterpreter) Interpret(interactive bool) error {
  188. if i.ParseArgs() {
  189. return nil
  190. }
  191. err := i.LoadStdlibPlugins(interactive)
  192. if err == nil {
  193. err = i.CreateTerm()
  194. if interactive {
  195. fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
  196. }
  197. // Create Runtime Provider
  198. if err == nil {
  199. if err = i.CreateRuntimeProvider("console"); err == nil {
  200. tid := i.RuntimeProvider.NewThreadID()
  201. if interactive {
  202. if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
  203. fmt.Fprint(i.LogOut, fmt.Sprintf("Log level: %v - ", lll.Level()))
  204. }
  205. fmt.Fprintln(i.LogOut, fmt.Sprintf("Root directory: %v", *i.Dir))
  206. if i.CustomWelcomeMessage != "" {
  207. fmt.Fprintln(i.LogOut, fmt.Sprintf(i.CustomWelcomeMessage))
  208. }
  209. }
  210. // Execute file if given
  211. if err = i.LoadInitialFile(tid); err == nil {
  212. // Drop into interactive shell
  213. if interactive {
  214. // Add history functionality without file persistence
  215. i.Term, err = termutil.AddHistoryMixin(i.Term, "",
  216. func(s string) bool {
  217. return i.isExitLine(s)
  218. })
  219. if err == nil {
  220. if err = i.Term.StartTerm(); err == nil {
  221. var line string
  222. defer i.Term.StopTerm()
  223. fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
  224. line, err = i.Term.NextLine()
  225. for err == nil && !i.isExitLine(line) {
  226. trimmedLine := strings.TrimSpace(line)
  227. i.HandleInput(i.Term, trimmedLine, tid)
  228. line, err = i.Term.NextLine()
  229. }
  230. }
  231. }
  232. }
  233. }
  234. }
  235. }
  236. }
  237. return err
  238. }
  239. /*
  240. LoadStdlibPlugins load plugins from .ecal.json.
  241. */
  242. func (i *CLIInterpreter) LoadStdlibPlugins(interactive bool) error {
  243. var err error
  244. if i.LoadPlugins {
  245. confFile := filepath.Join(*i.Dir, ".ecal.json")
  246. if ok, _ := fileutil.PathExists(confFile); ok {
  247. if interactive {
  248. fmt.Fprintln(i.LogOut, fmt.Sprintf("Loading stdlib plugins from %v", confFile))
  249. }
  250. var content []byte
  251. if content, err = ioutil.ReadFile(confFile); err == nil {
  252. var conf map[string]interface{}
  253. if err = json.Unmarshal(content, &conf); err == nil {
  254. if stdlibPlugins, ok := conf["stdlibPlugins"]; ok {
  255. err = fmt.Errorf("Config stdlibPlugins should be a list")
  256. if plugins, ok := stdlibPlugins.([]interface{}); ok {
  257. err = nil
  258. if errs := stdlib.LoadStdlibPlugins(plugins); len(errs) > 0 {
  259. for _, e := range errs {
  260. fmt.Fprintln(i.LogOut, fmt.Sprintf("Error loading plugins: %v", e))
  261. }
  262. err = fmt.Errorf("Could not load plugins defined in .ecal.json")
  263. }
  264. }
  265. }
  266. }
  267. }
  268. }
  269. }
  270. return err
  271. }
  272. /*
  273. isExitLine returns if a given input line should exit the interpreter.
  274. */
  275. func (i *CLIInterpreter) isExitLine(s string) bool {
  276. return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
  277. }
  278. /*
  279. HandleInput handles input to this interpreter. It parses a given input line
  280. and outputs on the given output terminal. Requires a thread ID of the executing
  281. thread - use the RuntimeProvider to generate a unique one.
  282. */
  283. func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64) {
  284. // Process the entered line
  285. if line == "?" {
  286. // Show help
  287. ot.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
  288. ot.WriteString(fmt.Sprint("\n"))
  289. ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
  290. ot.WriteString(fmt.Sprint("\n"))
  291. ot.WriteString(fmt.Sprint(" @format - Format all .ecal files in the current root directory.\n"))
  292. ot.WriteString(fmt.Sprint(" @prof [profile] - Output profiling information (supports any of Go's pprof profiles).\n"))
  293. ot.WriteString(fmt.Sprint(" @reload - Clear the interpreter and reload the initial file if it was given.\n"))
  294. ot.WriteString(fmt.Sprint(" @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
  295. ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
  296. if i.CustomHelpString != "" {
  297. ot.WriteString(i.CustomHelpString)
  298. }
  299. ot.WriteString(fmt.Sprint("\n"))
  300. ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
  301. } else if i.handleSpecialStatements(ot, line) {
  302. return
  303. } else if strings.HasPrefix(line, "@sym") {
  304. i.displaySymbols(ot, strings.Split(line, " ")[1:])
  305. } else if strings.HasPrefix(line, "@std") {
  306. i.displayPackage(ot, strings.Split(line, " ")[1:])
  307. } else if i.CustomHandler != nil && i.CustomHandler.CanHandle(line) {
  308. i.CustomHandler.Handle(ot, line)
  309. } else {
  310. var ierr error
  311. var ast *parser.ASTNode
  312. var res interface{}
  313. if line != "" {
  314. if ast, ierr = parser.ParseWithRuntime("console input", line, i.RuntimeProvider); ierr == nil {
  315. if ierr = ast.Runtime.Validate(); ierr == nil {
  316. if res, ierr = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid); ierr == nil && res != nil {
  317. ot.WriteString(fmt.Sprintln(stringutil.ConvertToString(res)))
  318. }
  319. defer func() {
  320. if i.RuntimeProvider.Debugger != nil {
  321. i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
  322. }
  323. }()
  324. }
  325. }
  326. if ierr != nil {
  327. ot.WriteString(fmt.Sprintln(ierr.Error()))
  328. }
  329. }
  330. }
  331. }
  332. /*
  333. handleSpecialStatements handles inbuild special statements.
  334. */
  335. func (i *CLIInterpreter) handleSpecialStatements(ot OutputTerminal, line string) bool {
  336. if strings.HasPrefix(line, "@prof") {
  337. args := strings.Split(line, " ")[1:]
  338. profile := "goroutine"
  339. if len(args) > 0 {
  340. profile = args[0]
  341. }
  342. var buf bytes.Buffer
  343. pprof.Lookup(profile).WriteTo(&buf, 1)
  344. ot.WriteString(buf.String())
  345. return true
  346. } else if strings.HasPrefix(line, "@format") {
  347. err := FormatFiles(*i.Dir, ".ecal")
  348. ot.WriteString(fmt.Sprintln(fmt.Sprintln("Files formatted:", err)))
  349. return true
  350. } else if strings.HasPrefix(line, "@reload") {
  351. // Reload happens in a separate thread as it may be suspended on start
  352. go func() {
  353. err := i.LoadInitialFile(i.RuntimeProvider.NewThreadID())
  354. ot.WriteString(fmt.Sprintln(fmt.Sprintln("Interpreter reloaded:", err)))
  355. }()
  356. ot.WriteString(fmt.Sprintln(fmt.Sprintln("Reloading interpreter state")))
  357. return true
  358. }
  359. return false
  360. }
  361. /*
  362. displaySymbols lists all available inbuild functions and available stdlib packages of ECAL.
  363. */
  364. func (i *CLIInterpreter) displaySymbols(ot OutputTerminal, args []string) {
  365. tabData := []string{"Inbuild function", "Description"}
  366. for name, f := range interpreter.InbuildFuncMap {
  367. ds, _ := f.DocString()
  368. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
  369. continue
  370. }
  371. tabData = fillTableRow(tabData, name, ds)
  372. }
  373. if len(tabData) > 2 {
  374. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  375. stringutil.SingleDoubleLineTable))
  376. }
  377. packageNames, _, _ := stdlib.GetStdlibSymbols()
  378. tabData = []string{"Package name", "Description"}
  379. for _, p := range packageNames {
  380. ps, _ := stdlib.GetPkgDocString(p)
  381. if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", p, ps), args[0]) {
  382. continue
  383. }
  384. tabData = fillTableRow(tabData, p, ps)
  385. }
  386. if len(tabData) > 2 {
  387. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  388. stringutil.SingleDoubleLineTable))
  389. }
  390. }
  391. /*
  392. displayPackage list all available constants and functions of a stdlib package.
  393. */
  394. func (i *CLIInterpreter) displayPackage(ot OutputTerminal, args []string) {
  395. _, constSymbols, funcSymbols := stdlib.GetStdlibSymbols()
  396. tabData := []string{"Constant", "Value"}
  397. for _, s := range constSymbols {
  398. if len(args) > 0 && !strings.HasPrefix(s, args[0]) {
  399. continue
  400. }
  401. val, _ := stdlib.GetStdlibConst(s)
  402. tabData = fillTableRow(tabData, s, fmt.Sprint(val))
  403. }
  404. if len(tabData) > 2 {
  405. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  406. stringutil.SingleDoubleLineTable))
  407. }
  408. tabData = []string{"Function", "Description"}
  409. for _, f := range funcSymbols {
  410. if len(args) > 0 && !strings.HasPrefix(f, args[0]) {
  411. continue
  412. }
  413. fObj, _ := stdlib.GetStdlibFunc(f)
  414. fDoc, _ := fObj.DocString()
  415. fDoc = strings.Replace(fDoc, "\n", " ", -1)
  416. fDoc = strings.Replace(fDoc, "\t", " ", -1)
  417. if len(args) > 1 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
  418. continue
  419. }
  420. tabData = fillTableRow(tabData, f, fDoc)
  421. }
  422. if len(tabData) > 2 {
  423. ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
  424. stringutil.SingleDoubleLineTable))
  425. }
  426. }