debugger.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package exprhelpers
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "github.com/antonmedv/expr"
  7. "github.com/antonmedv/expr/vm"
  8. log "github.com/sirupsen/logrus"
  9. )
  10. type ExprRuntimeDebug struct {
  11. Logger *log.Entry
  12. Lines []string
  13. Outputs []OpOutput
  14. }
  15. var IndentStep = 4
  16. // we use this struct to store the output of the expr runtime
  17. type OpOutput struct {
  18. Code string //relevant code part
  19. CodeDepth int //level of nesting
  20. BlockStart bool
  21. BlockEnd bool
  22. Func bool //true if it's a function call
  23. FuncName string
  24. Args []string
  25. FuncResults []string
  26. //
  27. Comparison bool //true if it's a comparison
  28. Negated bool
  29. Left string
  30. Right string
  31. //
  32. JumpIf bool //true if it's conditional jump
  33. IfTrue bool
  34. IfFalse bool
  35. //
  36. Condition bool //true if it's a condition
  37. ConditionIn bool
  38. ConditionContains bool
  39. //used for comparisons, conditional jumps and conditions
  40. StrConditionResult string
  41. ConditionResult *bool //should always be present for conditions
  42. //
  43. Finalized bool //used when a node is finalized, we already fetched result from next OP
  44. }
  45. func (o *OpOutput) String() string {
  46. ret := fmt.Sprintf("%*c", o.CodeDepth, ' ')
  47. if len(o.Code) != 0 {
  48. ret += fmt.Sprintf("[%s]", o.Code)
  49. }
  50. ret += " "
  51. switch {
  52. case o.BlockStart:
  53. ret = fmt.Sprintf("%*cBLOCK_START [%s]", o.CodeDepth-IndentStep, ' ', o.Code)
  54. return ret
  55. case o.BlockEnd:
  56. indent := o.CodeDepth - (IndentStep * 2)
  57. if indent < 0 {
  58. indent = 0
  59. }
  60. ret = fmt.Sprintf("%*cBLOCK_END [%s]", indent, ' ', o.Code)
  61. if len(o.StrConditionResult) > 0 {
  62. ret += fmt.Sprintf(" -> %s", o.StrConditionResult)
  63. }
  64. return ret
  65. //A block end can carry a value, for example if it's a count, any, all etc. XXX
  66. case o.Func:
  67. return ret + fmt.Sprintf("%s(%s) = %s", o.FuncName, strings.Join(o.Args, ", "), strings.Join(o.FuncResults, ", "))
  68. case o.Comparison:
  69. if o.Negated {
  70. ret += "NOT "
  71. }
  72. ret += fmt.Sprintf("%s == %s -> %s", o.Left, o.Right, o.StrConditionResult)
  73. return ret
  74. case o.ConditionIn:
  75. return ret + fmt.Sprintf("%s in %s -> %s", o.Args[0], o.Args[1], o.StrConditionResult)
  76. case o.ConditionContains:
  77. return ret + fmt.Sprintf("%s contains %s -> %s", o.Args[0], o.Args[1], o.StrConditionResult)
  78. case o.JumpIf && o.IfTrue:
  79. if o.ConditionResult != nil {
  80. if *o.ConditionResult {
  81. return ret + "OR -> false"
  82. }
  83. return ret + "OR -> true"
  84. }
  85. return ret + "OR(?)"
  86. case o.JumpIf && o.IfFalse:
  87. if o.ConditionResult != nil {
  88. if *o.ConditionResult {
  89. return ret + "AND -> true"
  90. }
  91. return ret + "AND -> false"
  92. }
  93. return ret + "AND(?)"
  94. }
  95. return ret + ""
  96. }
  97. func (erp ExprRuntimeDebug) extractCode(ip int, program *vm.Program, parts []string) string {
  98. //log.Tracef("# extracting code for ip %d [%s]", ip, parts[1])
  99. if program.Locations[ip].Line == 0 { //it seems line is zero when it's not actual code (ie. op push at the beginning)
  100. log.Tracef("zero location ?")
  101. return ""
  102. }
  103. startLine := program.Locations[ip].Line
  104. startColumn := program.Locations[ip].Column
  105. lines := strings.Split(program.Source.Content(), "\n")
  106. endCol := 0
  107. endLine := 0
  108. for i := ip + 1; i < len(program.Locations); i++ {
  109. if program.Locations[i].Line > startLine || (program.Locations[i].Line == startLine && program.Locations[i].Column > startColumn) {
  110. //we didn't had values yet and it's superior to current one, take it
  111. if endLine == 0 && endCol == 0 {
  112. endLine = program.Locations[i].Line
  113. endCol = program.Locations[i].Column
  114. }
  115. //however, we are looking for the closest upper one
  116. if program.Locations[i].Line < endLine || (program.Locations[i].Line == endLine && program.Locations[i].Column < endCol) {
  117. endLine = program.Locations[i].Line
  118. endCol = program.Locations[i].Column
  119. }
  120. }
  121. }
  122. //maybe it was the last instruction ?
  123. if endCol == 0 && endLine == 0 {
  124. endLine = len(lines)
  125. endCol = len(lines[endLine-1])
  126. }
  127. code_snippet := ""
  128. startLine -= 1 //line count starts at 1
  129. endLine -= 1
  130. for i := startLine; i <= endLine; i++ {
  131. if i == startLine {
  132. if startLine != endLine {
  133. code_snippet += lines[i][startColumn:]
  134. continue
  135. }
  136. code_snippet += lines[i][startColumn:endCol]
  137. break
  138. }
  139. if i == endLine {
  140. code_snippet += lines[i][:endCol]
  141. break
  142. }
  143. code_snippet += lines[i]
  144. }
  145. log.Tracef("#code extract for ip %d [%s] -> '%s'", ip, parts[1], code_snippet)
  146. return cleanTextForDebug(code_snippet)
  147. }
  148. func autoQuote(v any) string {
  149. switch x := v.(type) {
  150. case string:
  151. //let's avoid printing long strings. it can happen ie. when we are debugging expr with `File()` or similar helpers
  152. if len(x) > 40 {
  153. return fmt.Sprintf("%q", x[:40]+"...")
  154. } else {
  155. return fmt.Sprintf("%q", x)
  156. }
  157. default:
  158. return fmt.Sprintf("%v", x)
  159. }
  160. }
  161. func (erp ExprRuntimeDebug) ipDebug(ip int, vm *vm.VM, program *vm.Program, parts []string, outputs []OpOutput) ([]OpOutput, error) {
  162. IdxOut := len(outputs)
  163. prevIdxOut := 0
  164. currentDepth := 0
  165. //when there is a function call or comparison, we need to wait for the next instruction to get the result and "finalize" the previous one
  166. if IdxOut > 0 {
  167. prevIdxOut = IdxOut - 1
  168. currentDepth = outputs[prevIdxOut].CodeDepth
  169. if outputs[prevIdxOut].Func && !outputs[prevIdxOut].Finalized {
  170. stack := vm.Stack()
  171. num_items := 1
  172. for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
  173. outputs[prevIdxOut].FuncResults = append(outputs[prevIdxOut].FuncResults, autoQuote(stack[i]))
  174. num_items--
  175. }
  176. outputs[prevIdxOut].Finalized = true
  177. } else if (outputs[prevIdxOut].Comparison || outputs[prevIdxOut].Condition) && !outputs[prevIdxOut].Finalized {
  178. stack := vm.Stack()
  179. outputs[prevIdxOut].StrConditionResult = fmt.Sprintf("%+v", stack)
  180. if val, ok := stack[0].(bool); ok {
  181. outputs[prevIdxOut].ConditionResult = new(bool)
  182. *outputs[prevIdxOut].ConditionResult = val
  183. }
  184. outputs[prevIdxOut].Finalized = true
  185. }
  186. }
  187. erp.Logger.Tracef("[STEP %d:%s] (stack:%+v) (parts:%+v) {depth:%d}", ip, parts[1], vm.Stack(), parts, currentDepth)
  188. out := OpOutput{}
  189. out.CodeDepth = currentDepth
  190. out.Code = erp.extractCode(ip, program, parts)
  191. switch parts[1] {
  192. case "OpBegin":
  193. out.CodeDepth += IndentStep
  194. out.BlockStart = true
  195. outputs = append(outputs, out)
  196. case "OpEnd":
  197. out.CodeDepth -= IndentStep
  198. out.BlockEnd = true
  199. //OpEnd can carry value, if it's any/all/count etc.
  200. if len(vm.Stack()) > 0 {
  201. out.StrConditionResult = fmt.Sprintf("%v", vm.Stack())
  202. }
  203. outputs = append(outputs, out)
  204. case "OpNot":
  205. //negate the previous condition
  206. outputs[prevIdxOut].Negated = true
  207. case "OpTrue": //generated when possible ? (1 == 1)
  208. out.Condition = true
  209. out.ConditionResult = new(bool)
  210. *out.ConditionResult = true
  211. out.StrConditionResult = "true"
  212. outputs = append(outputs, out)
  213. case "OpFalse": //generated when possible ? (1 != 1)
  214. out.Condition = true
  215. out.ConditionResult = new(bool)
  216. *out.ConditionResult = false
  217. out.StrConditionResult = "false"
  218. outputs = append(outputs, out)
  219. case "OpJumpIfTrue": //OR
  220. stack := vm.Stack()
  221. out.JumpIf = true
  222. out.IfTrue = true
  223. out.StrConditionResult = fmt.Sprintf("%v", stack[0])
  224. if val, ok := stack[0].(bool); ok {
  225. out.ConditionResult = new(bool)
  226. *out.ConditionResult = val
  227. }
  228. outputs = append(outputs, out)
  229. case "OpJumpIfFalse": //AND
  230. stack := vm.Stack()
  231. out.JumpIf = true
  232. out.IfFalse = true
  233. out.StrConditionResult = fmt.Sprintf("%v", stack[0])
  234. if val, ok := stack[0].(bool); ok {
  235. out.ConditionResult = new(bool)
  236. *out.ConditionResult = val
  237. }
  238. outputs = append(outputs, out)
  239. case "OpCall1": //Op for function calls
  240. out.Func = true
  241. out.FuncName = parts[3]
  242. stack := vm.Stack()
  243. num_items := 1
  244. for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
  245. out.Args = append(out.Args, autoQuote(stack[i]))
  246. num_items--
  247. }
  248. outputs = append(outputs, out)
  249. case "OpCall2": //Op for function calls
  250. out.Func = true
  251. out.FuncName = parts[3]
  252. stack := vm.Stack()
  253. num_items := 2
  254. for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
  255. out.Args = append(out.Args, autoQuote(stack[i]))
  256. num_items--
  257. }
  258. outputs = append(outputs, out)
  259. case "OpCall3": //Op for function calls
  260. out.Func = true
  261. out.FuncName = parts[3]
  262. stack := vm.Stack()
  263. num_items := 3
  264. for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
  265. out.Args = append(out.Args, autoQuote(stack[i]))
  266. num_items--
  267. }
  268. outputs = append(outputs, out)
  269. //double check OpCallFast and OpCallTyped
  270. case "OpCallFast", "OpCallTyped":
  271. //
  272. case "OpCallN": //Op for function calls with more than 3 args
  273. out.Func = true
  274. out.FuncName = parts[1]
  275. stack := vm.Stack()
  276. //for OpCallN, we get the number of args
  277. if len(program.Arguments) >= ip {
  278. nb_args := program.Arguments[ip]
  279. if nb_args > 0 {
  280. //we need to skip the top item on stack
  281. for i := len(stack) - 2; i >= 0 && nb_args > 0; i-- {
  282. out.Args = append(out.Args, autoQuote(stack[i]))
  283. nb_args--
  284. }
  285. }
  286. } else { //let's blindly take the items on stack
  287. for _, val := range vm.Stack() {
  288. out.Args = append(out.Args, autoQuote(val))
  289. }
  290. }
  291. outputs = append(outputs, out)
  292. case "OpEqualString", "OpEqual", "OpEqualInt": //comparisons
  293. stack := vm.Stack()
  294. out.Comparison = true
  295. out.Left = autoQuote(stack[0])
  296. out.Right = autoQuote(stack[1])
  297. outputs = append(outputs, out)
  298. case "OpIn": //in operator
  299. stack := vm.Stack()
  300. out.Condition = true
  301. out.ConditionIn = true
  302. //seems that we tend to receive stack[1] as a map.
  303. //it is tempting to use reflect to extract keys, but we end up with an array that doesn't match the initial order
  304. //(because of the random order of the map)
  305. out.Args = append(out.Args, autoQuote(stack[0]))
  306. out.Args = append(out.Args, autoQuote(stack[1]))
  307. outputs = append(outputs, out)
  308. case "OpContains": //kind OpIn , but reverse
  309. stack := vm.Stack()
  310. out.Condition = true
  311. out.ConditionContains = true
  312. //seems that we tend to receive stack[1] as a map.
  313. //it is tempting to use reflect to extract keys, but we end up with an array that doesn't match the initial order
  314. //(because of the random order of the map)
  315. out.Args = append(out.Args, autoQuote(stack[0]))
  316. out.Args = append(out.Args, autoQuote(stack[1]))
  317. outputs = append(outputs, out)
  318. }
  319. return outputs, nil
  320. }
  321. func (erp ExprRuntimeDebug) ipSeek(ip int) []string {
  322. for i := 0; i < len(erp.Lines); i++ {
  323. parts := strings.Split(erp.Lines[i], "\t")
  324. if parts[0] == strconv.Itoa(ip) {
  325. return parts
  326. }
  327. }
  328. return nil
  329. }
  330. func Run(program *vm.Program, env interface{}, logger *log.Entry, debug bool) (any, error) {
  331. if debug {
  332. dbgInfo, ret, err := RunWithDebug(program, env, logger)
  333. DisplayExprDebug(program, dbgInfo, logger, ret)
  334. return ret, err
  335. }
  336. return expr.Run(program, env)
  337. }
  338. func cleanTextForDebug(text string) string {
  339. text = strings.Join(strings.Fields(text), " ")
  340. text = strings.Trim(text, " \t\n")
  341. return text
  342. }
  343. func DisplayExprDebug(program *vm.Program, outputs []OpOutput, logger *log.Entry, ret any) {
  344. logger.Debugf("dbg(result=%v): %s", ret, cleanTextForDebug(program.Source.Content()))
  345. for _, output := range outputs {
  346. logger.Debugf("%s", output.String())
  347. }
  348. }
  349. // TBD: Based on the level of the logger (ie. trace vs debug) we could decide to add more low level instructions (pop, push, etc.)
  350. func RunWithDebug(program *vm.Program, env interface{}, logger *log.Entry) ([]OpOutput, any, error) {
  351. var outputs []OpOutput = []OpOutput{}
  352. var buf strings.Builder
  353. var erp ExprRuntimeDebug = ExprRuntimeDebug{
  354. Logger: logger,
  355. }
  356. var debugErr chan error = make(chan error)
  357. vm := vm.Debug()
  358. done := false
  359. program.Opcodes(&buf)
  360. lines := strings.Split(buf.String(), "\n")
  361. erp.Lines = lines
  362. go func() {
  363. var err error
  364. erp.Logger.Tracef("[START] ip 0")
  365. ops := erp.ipSeek(0)
  366. if ops == nil {
  367. debugErr <- fmt.Errorf("failed getting ops for ip 0")
  368. return
  369. }
  370. if outputs, err = erp.ipDebug(0, vm, program, ops, outputs); err != nil {
  371. debugErr <- fmt.Errorf("error while debugging at ip 0")
  372. }
  373. vm.Step()
  374. for ip := range vm.Position() {
  375. ops := erp.ipSeek(ip)
  376. if ops == nil { //we reached the end of the program, we shouldn't throw an error
  377. erp.Logger.Tracef("[DONE] ip %d", ip)
  378. debugErr <- nil
  379. return
  380. }
  381. if outputs, err = erp.ipDebug(ip, vm, program, ops, outputs); err != nil {
  382. debugErr <- fmt.Errorf("error while debugging at ip %d", ip)
  383. return
  384. }
  385. if done {
  386. debugErr <- nil
  387. return
  388. }
  389. vm.Step()
  390. }
  391. debugErr <- nil
  392. }()
  393. var return_error error
  394. ret, err := vm.Run(program, env)
  395. done = true
  396. //if the expr runtime failed, we don't need to wait for the debug to finish
  397. if err != nil {
  398. return_error = err
  399. } else {
  400. err = <-debugErr
  401. if err != nil {
  402. log.Warningf("error while debugging expr: %s", err)
  403. }
  404. }
  405. //the overall result of expression is the result of last op ?
  406. if len(outputs) > 0 {
  407. lastOutIdx := len(outputs)
  408. if lastOutIdx > 0 {
  409. lastOutIdx -= 1
  410. }
  411. switch val := ret.(type) {
  412. case bool:
  413. log.Tracef("completing with bool %t", ret)
  414. //if outputs[lastOutIdx].Comparison {
  415. outputs[lastOutIdx].StrConditionResult = fmt.Sprintf("%v", ret)
  416. outputs[lastOutIdx].ConditionResult = new(bool)
  417. *outputs[lastOutIdx].ConditionResult = val
  418. outputs[lastOutIdx].Finalized = true
  419. default:
  420. log.Tracef("completing with type %T -> %v", ret, ret)
  421. outputs[lastOutIdx].StrConditionResult = fmt.Sprintf("%v", ret)
  422. outputs[lastOutIdx].Finalized = true
  423. }
  424. } else {
  425. log.Tracef("no output from expr runtime")
  426. }
  427. return outputs, ret, return_error
  428. }