diff options
Diffstat (limited to 'lib')
-rwxr-xr-x | lib/core/typeinfo.nim | 10 | ||||
-rwxr-xr-x | lib/system.nim | 19 | ||||
-rwxr-xr-x | lib/system/debugger.nim | 679 | ||||
-rw-r--r-- | lib/system/endb.nim | 538 |
4 files changed, 660 insertions, 586 deletions
diff --git a/lib/core/typeinfo.nim b/lib/core/typeinfo.nim index eb2a0c9e5..6f8081b5f 100755 --- a/lib/core/typeinfo.nim +++ b/lib/core/typeinfo.nim @@ -102,6 +102,16 @@ proc newAny(value: pointer, rawType: PNimType): TAny = result.value = value result.rawType = rawType +when defined(system.TVarSlot): + proc toAny*(x: TVarSlot): TAny {.inline.} = + ## constructs a ``TAny`` object from a variable slot ``x``. + ## This captures `x`'s address, so `x` can be modified with its + ## ``TAny`` wrapper! The client needs to ensure that the wrapper + ## **does not** live longer than `x`! + ## This is provided for easier reflection capabilities of a debugger. + result.value = x.address + result.rawType = x.typ + proc toAny*[T](x: var T): TAny {.inline.} = ## constructs a ``TAny`` object from `x`. This captures `x`'s address, so ## `x` can be modified with its ``TAny`` wrapper! The client needs to ensure diff --git a/lib/system.nim b/lib/system.nim index db78d2740..4aa7ecad5 100755 --- a/lib/system.nim +++ b/lib/system.nim @@ -1650,10 +1650,6 @@ const nimrodStackTrace = compileOption("stacktrace") # of the code var - dbgLineHook*: proc () {.nimcall.} - ## set this variable to provide a procedure that should be called before - ## each executed instruction. This should only be used by debuggers! - ## Only code compiled with the ``debugger:on`` switch calls this hook. globalRaiseHook*: proc (e: ref E_Base): bool {.nimcall.} ## with this hook you can influence exception handling on a global level. ## If not nil, every 'raise' statement ends up calling this hook. Ordinary @@ -1691,13 +1687,14 @@ var ## continues and the program is terminated. type - PFrame = ptr TFrame - TFrame {.importc, nodecl, final.} = object - prev: PFrame - procname: CString - line: int # current line number - filename: CString - len: int # length of slots (when not debugging always zero) + PFrame* = ptr TFrame ## represents a runtime frame of the call stack; + ## part of the debugger API. + TFrame* {.importc, nodecl, final.} = object ## the frame itself + prev*: PFrame ## previous frame; used for chaining the call stack + procname*: cstring ## name of the proc that is currently executing + line*: int ## line number of the proc that is currently executing + filename*: cstring ## filename of the proc that is currently executing + len*: int ## length of the inspectable slots when not defined(JS): {.push stack_trace:off, profiler:off.} diff --git a/lib/system/debugger.nim b/lib/system/debugger.nim index 62d667285..eade1707f 100755 --- a/lib/system/debugger.nim +++ b/lib/system/debugger.nim @@ -7,187 +7,95 @@ # distribution, for details about the copyright. # -# This file implements the embedded debugger that can be linked -# with the application. Mostly we do not use dynamic memory here as that -# would interfere with the GC and trigger ON/OFF errors if the -# user program corrupts memory. Unfortunately, for dispaying -# variables we use the ``system.repr()`` proc which uses Nimrod -# strings and thus allocates memory from the heap. Pity, but -# I do not want to implement ``repr()`` twice. +## This file implements basic features for any debugger. type - TStaticStr {.pure, final.} = object - len: int - data: array[0..100, char] - - TDbgState = enum - dbOff, # debugger is turned off - dbStepInto, # debugger is in tracing mode - dbStepOver, - dbSkipCurrent, - dbQuiting, # debugger wants to quit - dbBreakpoints # debugger is only interested in breakpoints - - TDbgBreakpoint {.final.} = object - low, high: int # range from low to high; if disabled - # both low and high are set to their negative values - # this makes the check faster and safes memory - filename: cstring - name: TStaticStr # name of breakpoint - - TVarSlot {.compilerproc, final.} = object # variable slots used for debugger: - address: pointer - typ: PNimType - name: cstring # for globals this is "module.name" + TVarSlot* {.compilerproc, final.} = object ## a slot in a frame + address*: pointer ## the variable's address + typ*: PNimType ## the variable's type + name*: cstring ## the variable's name; for globals this is "module.name" PExtendedFrame = ptr TExtendedFrame - TExtendedFrame {.final.} = object # If the debugger is enabled the compiler - # provides an extended frame. Of course - # only slots that are - # needed are allocated and not 10_000, - # except for the global data description. + TExtendedFrame = object # If the debugger is enabled the compiler + # provides an extended frame. Of course + # only slots that are + # needed are allocated and not 10_000, + # except for the global data description. f: TFrame slots: array[0..10_000, TVarSlot] var - dbgUser: TStaticStr # buffer for user input; first command is ``step_into`` - # needs to be global cause we store the last command - # in it - dbgState: TDbgState # state of debugger - dbgBP: array[0..127, TDbgBreakpoint] # breakpoints - dbgBPlen: int - dbgBPbloom: int64 # we use a bloom filter to speed up breakpoint checking - dbgSkipToFrame: PFrame # frame to be skipped to - dbgGlobalData: TExtendedFrame # this reserves much space, but # for now it is the most practical way - maxDisplayRecDepth: int = 5 # do not display too much data! +proc dbgRegisterGlobal(name: cstring, address: pointer, + typ: PNimType) {.compilerproc.} = + let i = dbgGlobalData.f.len + if i >= high(dbgGlobalData.slots): + #debugOut("[Warning] cannot register global ") + return + dbgGlobalData.slots[i].name = name + dbgGlobalData.slots[i].typ = typ + dbgGlobalData.slots[i].address = address + inc(dbgGlobalData.f.len) -proc setLen(s: var TStaticStr, newLen=0) = - s.len = newLen - s.data[newLen] = '\0' +proc getLocal*(frame: PFrame; slot: int): TVarSlot {.inline.} = + ## retrieves the meta data for the local variable at `slot`. CAUTION: An + ## invalid `slot` value causes a corruption! + result = cast[PExtendedFrame](frame).slots[slot] -proc add(s: var TStaticStr, c: char) = - if s.len < high(s.data)-1: - s.data[s.len] = c - s.data[s.len+1] = '\0' - inc s.len +proc getGlobalLen*(): int {.inline.} = + ## gets the number of registered globals. + result = dbgGlobalData.f.len -proc add(s: var TStaticStr, c: cstring) = - var i = 0 - while c[i] != '\0': - add s, c[i] - inc i - -proc assign(s: var TStaticStr, c: cstring) = - setLen(s) - add s, c - -proc `==`(a, b: TStaticStr): bool = - if a.len == b.len: - for i in 0 .. a.len-1: - if a.data[i] != b.data[i]: return false - return true - -proc `==`(a: TStaticStr, b: cstring): bool = - result = c_strcmp(a.data, b) == 0 - -proc findBreakpoint(name: TStaticStr): int = - # returns -1 if not found - for i in countdown(dbgBPlen-1, 0): - if name == dbgBP[i].name: return i - return -1 - -proc write(f: TFile, s: TStaticStr) = - write(f, cstring(s.data)) - -proc ListBreakPoints() = - write(stdout, "*** endb| Breakpoints:\n") - for i in 0 .. dbgBPlen-1: - write(stdout, dbgBP[i].name) - write(stdout, ": ") - write(stdout, abs(dbgBP[i].low)) - write(stdout, "..") - write(stdout, abs(dbgBP[i].high)) - write(stdout, dbgBP[i].filename) - if dbgBP[i].low < 0: - write(stdout, " [disabled]\n") - else: - write(stdout, "\n") - write(stdout, "***\n") - -proc openAppend(filename: cstring): TFile = - var p: pointer = fopen(filename, "ab") - if p != nil: - result = cast[TFile](p) - write(result, "----------------------------------------\n") - -proc dbgRepr(p: pointer, typ: PNimType): string = - var cl: TReprClosure - initReprClosure(cl) - cl.recDepth = maxDisplayRecDepth - # locks for the GC turned out to be a bad idea... - # inc(recGcLock) - result = "" - reprAux(result, p, typ, cl) - # dec(recGcLock) - deinitReprClosure(cl) - -proc writeVariable(stream: TFile, slot: TVarSlot) = - write(stream, slot.name) - write(stream, " = ") - writeln(stream, dbgRepr(slot.address, slot.typ)) - -proc ListFrame(stream: TFile, f: PExtendedFrame) = - write(stream, "*** endb| Frame (") - write(stream, f.f.len) - write(stream, " slots):\n") - for i in 0 .. f.f.len-1: - writeVariable(stream, f.slots[i]) - write(stream, "***\n") - -proc ListVariables(stream: TFile, f: PExtendedFrame) = - write(stream, "*** endb| Frame (") - write(stream, f.f.len) - write(stream, " slots):\n") - for i in 0 .. f.f.len-1: - writeln(stream, f.slots[i].name) - write(stream, "***\n") - -proc debugOut(msg: cstring) = - # the *** *** markers are for easy recognition of debugger - # output for external frontends. - write(stdout, "*** endb| ") - write(stdout, msg) - write(stdout, "***\n") - -proc dbgFatal(msg: cstring) = - debugOut(msg) - dbgAborting = True # the debugger wants to abort - quit(1) - -proc findVariable(frame: PExtendedFrame, varname: cstring): int = - for i in 0 .. frame.f.len - 1: - if c_strcmp(frame.slots[i].name, varname) == 0: return i - return -1 - -proc dbgShowCurrentProc(dbgFramePointer: PFrame) = - if dbgFramePointer != nil: - write(stdout, "*** endb| now in proc: ") - write(stdout, dbgFramePointer.procname) - write(stdout, " ***\n") - else: - write(stdout, "*** endb| (proc name not available) ***\n") +proc getGlobal*(slot: int): TVarSlot {.inline.} = + ## retrieves the meta data for the global variable at `slot`. CAUTION: An + ## invalid `slot` value causes a corruption! + result = dbgGlobalData.slots[slot] + +# ------------------- breakpoint support ------------------------------------ -proc dbgShowExecutionPoint() = - write(stdout, "*** endb| ") - write(stdout, framePtr.filename) - write(stdout, "(") - write(stdout, framePtr.line) - write(stdout, ") ") - write(stdout, framePtr.procname) - write(stdout, " ***\n") +type + TBreakpoint* = object ## represents a break point + low*, high*: int ## range from low to high; if disabled + ## both low and high are set to their negative values + filename*: cstring ## the filename of the breakpoint + +var + dbgBP: array[0..127, TBreakpoint] # breakpoints + dbgBPlen: int + dbgBPbloom: int64 # we use a bloom filter to speed up breakpoint checking + + dbgFilenames*: array[0..300, cstring] ## registered filenames; + ## 'nil' terminated + dbgFilenameLen: int + +proc dbgRegisterFilename(filename: cstring) {.compilerproc.} = + # XXX we could check for duplicates here for DLL support + dbgFilenames[dbgFilenameLen] = filename + inc dbgFilenameLen + +proc dbgRegisterBreakpoint(line: int, + filename, name: cstring) {.compilerproc.} = + let x = dbgBPlen + if x >= high(dbgBP): + #debugOut("[Warning] cannot register breakpoint") + return + inc(dbgBPlen) + dbgBP[x].filename = filename + dbgBP[x].low = line + dbgBP[x].high = line + dbgBPbloom = dbgBPbloom or line + +proc addBreakpoint*(filename: cstring, lo, hi: int): bool = + let x = dbgBPlen + if x >= high(dbgBP): return false + inc(dbgBPlen) + result = true + dbgBP[x].filename = filename + dbgBP[x].low = lo + dbgBP[x].high = hi + for line in lo..hi: dbgBPbloom = dbgBPbloom or line const FileSystemCaseInsensitive = defined(windows) or defined(dos) or defined(os2) @@ -216,337 +124,29 @@ proc fileMatches(c, bp: cstring): bool = inc(i) return true -proc dbgBreakpointReached(line: int): int = - for i in 0..dbgBPlen-1: - if line >= dbgBP[i].low and line <= dbgBP[i].high and - fileMatches(framePtr.filename, dbgBP[i].filename): return i - return -1 - -proc scanAndAppendWord(src: cstring, a: var TStaticStr, start: int): int = - result = start - # skip whitespace: - while src[result] in {'\t', ' '}: inc(result) - while True: - case src[result] - of 'a'..'z', '0'..'9': add(a, src[result]) - of '_': nil # just skip it - of 'A'..'Z': add(a, chr(ord(src[result]) - ord('A') + ord('a'))) - else: break - inc(result) - -proc scanWord(src: cstring, a: var TStaticStr, start: int): int = - setlen(a) - result = scanAndAppendWord(src, a, start) - -proc scanFilename(src: cstring, a: var TStaticStr, start: int): int = - result = start - setLen a - # skip whitespace: - while src[result] in {'\t', ' '}: inc(result) - while src[result] notin {'\t', ' ', '\0'}: - add(a, src[result]) - inc(result) - -proc scanNumber(src: cstring, a: var int, start: int): int = - result = start - a = 0 - while src[result] in {'\t', ' '}: inc(result) - while true: - case src[result] - of '0'..'9': a = a * 10 + ord(src[result]) - ord('0') - of '_': nil # skip underscores (nice for long line numbers) - else: break - inc(result) - -proc dbgHelp() = - debugOut(""" -list of commands (see the manual for further help): - GENERAL -h, help display this help message -q, quit quit the debugger and the program -<ENTER> repeat the previous debugger command - EXECUTING -s, step single step, stepping into routine calls -n, next single step, without stepping into routine calls -f, skipcurrent continue execution until the current routine finishes -c, continue, r, run continue execution until the next breakpoint -i, ignore continue execution, ignore all breakpoints - BREAKPOINTS -b, break <name> [fromline [toline]] [file] - set a new breakpoint named 'name' for line and file - if line or file are omitted the current one is used -breakpoints display the entire breakpoint list -disable <name> disable a breakpoint -enable <name> enable a breakpoint - DATA DISPLAY -e, eval <expr> evaluate the expression <expr> -o, out <file> <expr> evaluate <expr> and write it to <file> -w, where display the current execution point -stackframe [file] display current stack frame [and write it to file] -u, up go up in the call stack -d, down go down in the call stack -bt, backtrace display the entire call stack -l, locals display available local variables -g, globals display available global variables -maxdisplay <integer> set the display's recursion maximum -""") - -proc InvalidCommand() = - debugOut("[Warning] invalid command ignored (type 'h' for help) ") - -proc hasExt(s: cstring): bool = - # returns true if s has a filename extension - var i = 0 - while s[i] != '\0': - if s[i] == '.': return true - inc i - -proc setBreakPoint(s: cstring, start: int) = - var dbgTemp: TStaticStr - var i = scanWord(s, dbgTemp, start) - if i <= start: - InvalidCommand() - return - if dbgBPlen >= high(dbgBP): - debugOut("[Warning] no breakpoint could be set; out of breakpoint space ") - return - var x = dbgBPlen - inc(dbgBPlen) - dbgBP[x].name = dbgTemp - i = scanNumber(s, dbgBP[x].low, i) - if dbgBP[x].low == 0: - # set to current line: - dbgBP[x].low = framePtr.line - i = scanNumber(s, dbgBP[x].high, i) - if dbgBP[x].high == 0: # set to low: - dbgBP[x].high = dbgBP[x].low - for line in dbgBP[x].low .. dbgBP[x].high: dbgBPbloom = dbgBPbloom or line - i = scanFilename(s, dbgTemp, i) - if dbgTemp.len != 0: - debugOut("[Warning] explicit filename for breakpoint not supported") - when false: - if not hasExt(dbgTemp.data): add(dbgTemp, ".nim") - dbgBP[x].filename = dbgTemp - dbgBP[x].filename = framePtr.filename - else: # use current filename - dbgBP[x].filename = framePtr.filename - # skip whitespace: - while s[i] in {' ', '\t'}: inc(i) - if s[i] != '\0': - dec(dbgBPLen) # remove buggy breakpoint - InvalidCommand() - -proc BreakpointSetEnabled(s: cstring, start, enabled: int) = - var dbgTemp: TStaticStr - var i = scanWord(s, dbgTemp, start) - if i <= start: - InvalidCommand() - return - var x = findBreakpoint(dbgTemp) - if x < 0: debugOut("[Warning] breakpoint does not exist ") - elif enabled * dbgBP[x].low < 0: # signs are different? - dbgBP[x].low = -dbgBP[x].low - dbgBP[x].high = -dbgBP[x].high - -proc dbgEvaluate(stream: TFile, s: cstring, start: int, - currFrame: PExtendedFrame) = - var dbgTemp: tstaticstr - var i = scanWord(s, dbgTemp, start) - while s[i] in {' ', '\t'}: inc(i) - var f = currFrame - if s[i] == '.': - inc(i) # skip '.' - add(dbgTemp, '.') - i = scanAndAppendWord(s, dbgTemp, i) - # search for global var: - f = addr(dbgGlobalData) - if s[i] != '\0': - debugOut("[Warning] could not parse expr ") - return - var j = findVariable(f, dbgTemp.data) - if j < 0: - debugOut("[Warning] could not find variable ") - return - writeVariable(stream, f.slots[j]) +proc canonFilename*(filename: cstring): cstring = + ## returns 'nil' if the filename cannot be found. + for i in 0 .. <dbgFilenameLen: + result = dbgFilenames[i] + if fileMatches(result, filename): return result + result = nil -proc dbgOut(s: cstring, start: int, currFrame: PExtendedFrame) = - var dbgTemp: tstaticstr - var i = scanFilename(s, dbgTemp, start) - if dbgTemp.len == 0: - InvalidCommand() - return - var stream = openAppend(dbgTemp.data) - if stream == nil: - debugOut("[Warning] could not open or create file ") - return - dbgEvaluate(stream, s, i, currFrame) - close(stream) - -proc dbgStackFrame(s: cstring, start: int, currFrame: PExtendedFrame) = - var dbgTemp: TStaticStr - var i = scanFilename(s, dbgTemp, start) - if dbgTemp.len == 0: - # just write it to stdout: - ListFrame(stdout, currFrame) - else: - var stream = openAppend(dbgTemp.data) - if stream == nil: - debugOut("[Warning] could not open or create file ") - return - ListFrame(stream, currFrame) - close(stream) - -proc readLine(f: TFile, line: var TStaticStr): bool = - while True: - var c = fgetc(f) - if c < 0'i32: - if line.len > 0: break - else: return false - if c == 10'i32: break # LF - if c == 13'i32: # CR - c = fgetc(f) # is the next char LF? - if c != 10'i32: ungetc(c, f) # no, put the character back - break - add line, chr(int(c)) - result = true +iterator listBreakpoints*(): ptr TBreakpoint = + ## lists all breakpoints. + for i in 0..dbgBPlen-1: yield addr(dbgBP[i]) -proc dbgWriteStackTrace(f: PFrame) -proc CommandPrompt() = - # if we return from this routine, user code executes again - var - again = True - dbgFramePtr = framePtr # for going down and up the stack - dbgDown = 0 # how often we did go down - dbgTemp: TStaticStr - - while again: - write(stdout, "*** endb| >>") - let oldLen = dbgUser.len - dbgUser.len = 0 - if not readLine(stdin, dbgUser): break - if dbgUser.len == 0: dbgUser.len = oldLen - # now look what we have to do: - var i = scanWord(dbgUser.data, dbgTemp, 0) - template `?`(x: expr): expr = dbgTemp == cstring(x) - if ?"s" or ?"step": - dbgState = dbStepInto - again = false - elif ?"n" or ?"next": - dbgState = dbStepOver - dbgSkipToFrame = framePtr - again = false - elif ?"f" or ?"skipcurrent": - dbgState = dbSkipCurrent - dbgSkipToFrame = framePtr.prev - again = false - elif ?"c" or ?"continue" or ?"r" or ?"run": - dbgState = dbBreakpoints - again = false - elif ?"i" or ?"ignore": - dbgState = dbOff - again = false - elif ?"h" or ?"help": - dbgHelp() - elif ?"q" or ?"quit": - dbgState = dbQuiting - dbgAborting = True - again = false - quit(1) # BUGFIX: quit with error code > 0 - elif ?"e" or ?"eval": - dbgEvaluate(stdout, dbgUser.data, i, cast[PExtendedFrame](dbgFramePtr)) - elif ?"o" or ?"out": - dbgOut(dbgUser.data, i, cast[PExtendedFrame](dbgFramePtr)) - elif ?"stackframe": - dbgStackFrame(dbgUser.data, i, cast[PExtendedFrame](dbgFramePtr)) - elif ?"w" or ?"where": - dbgShowExecutionPoint() - elif ?"l" or ?"locals": - ListVariables(stdout, cast[PExtendedFrame](dbgFramePtr)) - elif ?"g" or ?"globals": - ListVariables(stdout, addr(dbgGlobalData)) - elif ?"u" or ?"up": - if dbgDown <= 0: - debugOut("[Warning] cannot go up any further ") - else: - dbgFramePtr = framePtr - for j in 0 .. dbgDown-2: # BUGFIX - dbgFramePtr = dbgFramePtr.prev - dec(dbgDown) - dbgShowCurrentProc(dbgFramePtr) - elif ?"d" or ?"down": - if dbgFramePtr != nil: - inc(dbgDown) - dbgFramePtr = dbgFramePtr.prev - dbgShowCurrentProc(dbgFramePtr) - else: - debugOut("[Warning] cannot go down any further ") - elif ?"bt" or ?"backtrace": - dbgWriteStackTrace(framePtr) - elif ?"b" or ?"break": - setBreakPoint(dbgUser.data, i) - elif ?"breakpoints": - ListBreakPoints() - elif ?"disable": - BreakpointSetEnabled(dbgUser.data, i, -1) - elif ?"enable": - BreakpointSetEnabled(dbgUser.data, i, +1) - elif ?"maxdisplay": - var parsed: int - i = scanNumber(dbgUser.data, parsed, i) - if dbgUser.data[i-1] in {'0'..'9'}: - if parsed == 0: maxDisplayRecDepth = -1 - else: maxDisplayRecDepth = parsed - else: - InvalidCommand() - else: InvalidCommand() - -proc endbStep() = - # we get into here if an unhandled exception has been raised - # XXX: do not allow the user to run the program any further? - # XXX: BUG: the frame is lost here! - dbgShowExecutionPoint() - CommandPrompt() - -proc checkForBreakpoint(line: int) = - if (dbgBPbloom and line) != line: return - let i = dbgBreakpointReached(line) - if i >= 0: - write(stdout, "*** endb| reached ") - write(stdout, dbgBP[i].name) - write(stdout, " in ") - write(stdout, framePtr.filename) - write(stdout, "(") - write(stdout, framePtr.line) - write(stdout, ") ") - write(stdout, framePtr.procname) - write(stdout, " ***\n") - CommandPrompt() - -# interface to the user program: +proc isActive*(b: ptr TBreakpoint): bool = b.low > 0 +proc flip*(b: ptr TBreakpoint) = + ## enables or disables 'b' depending on its current state. + b.low = -b.low; b.high = -b.high -proc dbgRegisterBreakpoint(line: int, - filename, name: cstring) {.compilerproc.} = - let x = dbgBPlen - if x >= high(dbgBP): - debugOut("[Warning] cannot register breakpoint") - return - inc(dbgBPlen) - dbgBP[x].name.assign(name) - dbgBP[x].filename = filename - dbgBP[x].low = line - dbgBP[x].high = line - dbgBPbloom = dbgBPbloom or line +proc checkBreakpoints*(filename: cstring, line: int): ptr TBreakpoint = + ## in which breakpoint (if any) we are. + if (dbgBPbloom and line) != line: return nil + for b in listBreakpoints(): + if line >= b.low and line <= b.high and filename == b.filename: return b -proc dbgRegisterGlobal(name: cstring, address: pointer, - typ: PNimType) {.compilerproc.} = - let i = dbgGlobalData.f.len - if i >= high(dbgGlobalData.slots): - debugOut("[Warning] cannot register global ") - return - dbgGlobalData.slots[i].name = name - dbgGlobalData.slots[i].typ = typ - dbgGlobalData.slots[i].address = address - inc(dbgGlobalData.f.len) +# ------------------- watchpoint support ------------------------------------ type THash = int @@ -665,7 +265,7 @@ proc dbgRegisterWatchpoint(address: pointer, name: cstring, Watchpoints[i].address = address return if L >= watchPoints.high: - debugOut("[Warning] cannot register watchpoint") + #debugOut("[Warning] cannot register watchpoint") return Watchpoints[L].name = name Watchpoints[L].address = address @@ -676,99 +276,28 @@ proc dbgRegisterWatchpoint(address: pointer, name: cstring, proc dbgUnregisterWatchpoints*() = WatchpointsLen = 0 -proc dbgWriteStackTrace(f: PFrame) = - const - firstCalls = 32 - var - it = f - i = 0 - total = 0 - tempFrames: array [0..127, PFrame] - # setup long head: - while it != nil and i <= high(tempFrames)-firstCalls: - tempFrames[i] = it - inc(i) - inc(total) - it = it.prev - # go up the stack to count 'total': - var b = it - while it != nil: - inc(total) - it = it.prev - var skipped = 0 - if total > len(tempFrames): - # skip N - skipped = total-i-firstCalls+1 - for j in 1..skipped: - if b != nil: b = b.prev - # create '...' entry: - tempFrames[i] = nil - inc(i) - # setup short tail: - while b != nil and i <= high(tempFrames): - tempFrames[i] = b - inc(i) - b = b.prev - for j in countdown(i-1, 0): - if tempFrames[j] == nil: - write(stdout, "(") - write(stdout, skipped) - write(stdout, " calls omitted) ...") - else: - write(stdout, tempFrames[j].filename) - if tempFrames[j].line > 0: - write(stdout, '(') - write(stdout, tempFrames[j].line) - write(stdout, ')') - write(stdout, ' ') - write(stdout, tempFrames[j].procname) - write(stdout, "\n") - -proc strstr(s1, s2: cstring): cstring {.importc, header: "<string.h>".} +var + dbgLineHook*: proc () {.nimcall.} + ## set this variable to provide a procedure that should be called before + ## each executed instruction. This should only be used by debuggers! + ## Only code compiled with the ``debugger:on`` switch calls this hook. -proc interestingFilename(filename: cstring): bool = - #result = strstr(filename, "/rst.nim") == nil - result = true + dbgWatchpointHook*: proc (watchpointName: cstring) {.nimcall.} proc checkWatchpoints = let L = WatchpointsLen for i in 0.. <L: let newHash = genericHash(Watchpoints[i].address, Watchpoints[i].typ) if newHash != Watchpoints[i].oldValue: - if interestingFilename(framePtr.filename): - dbgWriteStackTrace(framePtr) - debugOut(Watchpoints[i].name) + dbgWatchpointHook(Watchpoints[i].name) Watchpoints[i].oldValue = newHash -proc endb(line: int, file: cstring) {.compilerproc.} = +proc endb(line: int, file: cstring) {.compilerproc, noinline.} = # This proc is called before every Nimrod code line! - # Thus, it must have as few parameters as possible to keep the - # code size small! - # Check if we are at an enabled breakpoint or "in the mood" if framePtr == nil: return - let oldState = dbgState - checkWatchpoints() + if dbgWatchpointHook != nil: checkWatchpoints() framePtr.line = line # this is done here for smaller code size! framePtr.filename = file if dbgLineHook != nil: dbgLineHook() - case oldState - of dbStepInto: - # we really want the command prompt here: - dbgShowExecutionPoint() - CommandPrompt() - of dbSkipCurrent, dbStepOver: # skip current routine - if framePtr == dbgSkipToFrame: - dbgShowExecutionPoint() - CommandPrompt() - else: # breakpoints are wanted though (I guess) - checkForBreakpoint(line) - of dbBreakpoints: - # debugger is only interested in breakpoints - checkForBreakpoint(line) - else: nil - -proc initDebugger {.inline.} = - dbgState = dbStepInto - dbgUser.len = 1 - dbgUser.data[0] = 's' +include "system/endb" diff --git a/lib/system/endb.nim b/lib/system/endb.nim new file mode 100644 index 000000000..2d6a25824 --- /dev/null +++ b/lib/system/endb.nim @@ -0,0 +1,538 @@ +# +# +# Nimrod's Runtime Library +# (c) Copyright 2013 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +# This file implements the embedded debugger that can be linked +# with the application. Mostly we do not use dynamic memory here as that +# would interfere with the GC and trigger ON/OFF errors if the +# user program corrupts memory. Unfortunately, for dispaying +# variables we use the ``system.repr()`` proc which uses Nimrod +# strings and thus allocates memory from the heap. Pity, but +# I do not want to implement ``repr()`` twice. + +const + EndbBeg = "*** endb" + EndbEnd = "***\n" + +type + TStaticStr = object + len: int + data: array[0..100, char] + + TBreakpointFilename = object + b: ptr TBreakpoint + filename: TStaticStr + + TDbgState = enum + dbOff, # debugger is turned off + dbStepInto, # debugger is in tracing mode + dbStepOver, + dbSkipCurrent, + dbQuiting, # debugger wants to quit + dbBreakpoints # debugger is only interested in breakpoints + +var + dbgUser: TStaticStr # buffer for user input; first command is ``step_into`` + # needs to be global cause we store the last command + # in it + dbgState: TDbgState # state of debugger + dbgSkipToFrame: PFrame # frame to be skipped to + + maxDisplayRecDepth: int = 5 # do not display too much data! + + brkPoints: array[0..127, TBreakpointFilename] + +proc setLen(s: var TStaticStr, newLen=0) = + s.len = newLen + s.data[newLen] = '\0' + +proc add(s: var TStaticStr, c: char) = + if s.len < high(s.data)-1: + s.data[s.len] = c + s.data[s.len+1] = '\0' + inc s.len + +proc add(s: var TStaticStr, c: cstring) = + var i = 0 + while c[i] != '\0': + add s, c[i] + inc i + +proc assign(s: var TStaticStr, c: cstring) = + setLen(s) + add s, c + +proc `==`(a, b: TStaticStr): bool = + if a.len == b.len: + for i in 0 .. a.len-1: + if a.data[i] != b.data[i]: return false + return true + +proc `==`(a: TStaticStr, b: cstring): bool = + result = c_strcmp(a.data, b) == 0 + +proc write(f: TFile, s: TStaticStr) = + write(f, cstring(s.data)) + +proc ListBreakPoints() = + write(stdout, EndbBeg) + write(stdout, "| Breakpoints:\n") + for b in listBreakpoints(): + write(stdout, abs(b.low)) + if b.high != b.low: + write(stdout, "..") + write(stdout, abs(b.high)) + write(stdout, " ") + write(stdout, b.filename) + if b.isActive: + write(stdout, " [disabled]\n") + else: + write(stdout, "\n") + write(stdout, EndbEnd) + +proc openAppend(filename: cstring): TFile = + var p: pointer = fopen(filename, "ab") + if p != nil: + result = cast[TFile](p) + write(result, "----------------------------------------\n") + +proc dbgRepr(p: pointer, typ: PNimType): string = + var cl: TReprClosure + initReprClosure(cl) + cl.recDepth = maxDisplayRecDepth + # locks for the GC turned out to be a bad idea... + # inc(recGcLock) + result = "" + reprAux(result, p, typ, cl) + # dec(recGcLock) + deinitReprClosure(cl) + +proc writeVariable(stream: TFile, slot: TVarSlot) = + write(stream, slot.name) + write(stream, " = ") + writeln(stream, dbgRepr(slot.address, slot.typ)) + +proc ListFrame(stream: TFile, f: PFrame) = + write(stream, EndbBeg) + write(stream, "| Frame (") + write(stream, f.len) + write(stream, " slots):\n") + for i in 0 .. f.len-1: + writeln(stream, getLocal(f, i).name) + write(stream, EndbEnd) + +proc ListLocals(stream: TFile, f: PFrame) = + write(stream, EndbBeg) + write(stream, "| Frame (") + write(stream, f.len) + write(stream, " slots):\n") + for i in 0 .. f.len-1: + writeVariable(stream, getLocal(f, i)) + write(stream, EndbEnd) + +proc ListGlobals(stream: TFile) = + write(stream, EndbBeg) + write(stream, "| Globals:\n") + for i in 0 .. getGlobalLen()-1: + writeln(stream, getGlobal(i).name) + write(stream, EndbEnd) + +proc debugOut(msg: cstring) = + # the *** *** markers are for easy recognition of debugger + # output for external frontends. + write(stdout, EndbBeg) + write(stdout, "| ") + write(stdout, msg) + write(stdout, EndbEnd) + +proc dbgFatal(msg: cstring) = + debugOut(msg) + dbgAborting = True # the debugger wants to abort + quit(1) + +proc dbgShowCurrentProc(dbgFramePointer: PFrame) = + if dbgFramePointer != nil: + write(stdout, "*** endb| now in proc: ") + write(stdout, dbgFramePointer.procname) + write(stdout, " ***\n") + else: + write(stdout, "*** endb| (proc name not available) ***\n") + +proc dbgShowExecutionPoint() = + write(stdout, "*** endb| ") + write(stdout, framePtr.filename) + write(stdout, "(") + write(stdout, framePtr.line) + write(stdout, ") ") + write(stdout, framePtr.procname) + write(stdout, " ***\n") + +proc scanAndAppendWord(src: cstring, a: var TStaticStr, start: int): int = + result = start + # skip whitespace: + while src[result] in {'\t', ' '}: inc(result) + while True: + case src[result] + of 'a'..'z', '0'..'9': add(a, src[result]) + of '_': nil # just skip it + of 'A'..'Z': add(a, chr(ord(src[result]) - ord('A') + ord('a'))) + else: break + inc(result) + +proc scanWord(src: cstring, a: var TStaticStr, start: int): int = + setlen(a) + result = scanAndAppendWord(src, a, start) + +proc scanFilename(src: cstring, a: var TStaticStr, start: int): int = + result = start + setLen a + while src[result] in {'\t', ' '}: inc(result) + while src[result] notin {'\t', ' ', '\0'}: + add(a, src[result]) + inc(result) + +proc scanNumber(src: cstring, a: var int, start: int): int = + result = start + a = 0 + while src[result] in {'\t', ' '}: inc(result) + while true: + case src[result] + of '0'..'9': a = a * 10 + ord(src[result]) - ord('0') + of '_': nil # skip underscores (nice for long line numbers) + else: break + inc(result) + +proc dbgHelp() = + debugOut(""" +list of commands (see the manual for further help): + GENERAL +h, help display this help message +q, quit quit the debugger and the program +<ENTER> repeat the previous debugger command + EXECUTING +s, step single step, stepping into routine calls +n, next single step, without stepping into routine calls +f, skipcurrent continue execution until the current routine finishes +c, continue, r, run continue execution until the next breakpoint +i, ignore continue execution, ignore all breakpoints + BREAKPOINTS +b, break [fromline [toline]] [file] + set a new breakpoint for line and file + if line or file are omitted the current one is used +breakpoints display the entire breakpoint list +toggle fromline [file] enable or disable a breakpoint +filenames list all valid filenames + DATA DISPLAY +e, eval <expr> evaluate the expression <expr> +o, out <file> <expr> evaluate <expr> and write it to <file> +w, where display the current execution point +stackframe [file] display current stack frame [and write it to file] +u, up go up in the call stack +d, down go down in the call stack +bt, backtrace display the entire call stack +l, locals display available local variables +g, globals display available global variables +maxdisplay <integer> set the display's recursion maximum +""") + +proc InvalidCommand() = + debugOut("[Warning] invalid command ignored (type 'h' for help) ") + +proc hasExt(s: cstring): bool = + # returns true if s has a filename extension + var i = 0 + while s[i] != '\0': + if s[i] == '.': return true + inc i + +proc parseBreakpoint(s: cstring, start: int): TBreakpoint = + var dbgTemp: TStaticStr + var i = scanNumber(s, result.low, start) + if result.low == 0: result.low = framePtr.line + i = scanNumber(s, result.high, i) + if result.high == 0: result.high = result.low + i = scanFilename(s, dbgTemp, i) + if dbgTemp.len != 0: + if not hasExt(dbgTemp.data): add(dbgTemp, ".nim") + result.filename = canonFilename(dbgTemp.data.cstring) + if result.filename.isNil: + debugOut("[Warning] no breakpoint could be set; unknown filename ") + return + else: + result.filename = framePtr.filename + +proc createBreakPoint(s: cstring, start: int) = + let br = parseBreakpoint(s, start) + if not br.filename.isNil: + if not addBreakpoint(br.filename, br.low, br.high): + debugOut("[Warning] no breakpoint could be set; out of breakpoint space ") + +proc BreakpointToggle(s: cstring, start: int) = + var a = parseBreakpoint(s, start) + if not a.filename.isNil: + var b = checkBreakpoints(a.filename, a.low) + if not b.isNil: b.flip + else: debugOut("[Warning] unknown breakpoint ") + +proc dbgEvaluate(stream: TFile, s: cstring, start: int, f: PFrame) = + var dbgTemp: tstaticstr + var i = scanWord(s, dbgTemp, start) + while s[i] in {' ', '\t'}: inc(i) + var v: TVarSlot + if s[i] == '.': + inc(i) + add(dbgTemp, '.') + i = scanAndAppendWord(s, dbgTemp, i) + for i in 0 .. getGlobalLen()-1: + let v = getGlobal(i) + if c_strcmp(v.name, dbgTemp.data) == 0: + writeVariable(stream, v) + else: + for i in 0 .. f.len-1: + let v = getLocal(f, i) + if c_strcmp(v.name, dbgTemp.data) == 0: + writeVariable(stream, v) + +proc dbgOut(s: cstring, start: int, currFrame: PFrame) = + var dbgTemp: tstaticstr + var i = scanFilename(s, dbgTemp, start) + if dbgTemp.len == 0: + InvalidCommand() + return + var stream = openAppend(dbgTemp.data) + if stream == nil: + debugOut("[Warning] could not open or create file ") + return + dbgEvaluate(stream, s, i, currFrame) + close(stream) + +proc dbgStackFrame(s: cstring, start: int, currFrame: PFrame) = + var dbgTemp: TStaticStr + var i = scanFilename(s, dbgTemp, start) + if dbgTemp.len == 0: + # just write it to stdout: + ListFrame(stdout, currFrame) + else: + var stream = openAppend(dbgTemp.data) + if stream == nil: + debugOut("[Warning] could not open or create file ") + return + ListFrame(stream, currFrame) + close(stream) + +proc readLine(f: TFile, line: var TStaticStr): bool = + while True: + var c = fgetc(f) + if c < 0'i32: + if line.len > 0: break + else: return false + if c == 10'i32: break # LF + if c == 13'i32: # CR + c = fgetc(f) # is the next char LF? + if c != 10'i32: ungetc(c, f) # no, put the character back + break + add line, chr(int(c)) + result = true + +proc ListFilenames() = + write(stdout, EndbBeg) + write(stdout, "| Files:\n") + var i = 0 + while true: + let x = dbgFilenames[i] + if x.isNil: break + write(stdout, x) + write(stdout, "\n") + inc i + write(stdout, EndbEnd) + +proc dbgWriteStackTrace(f: PFrame) +proc CommandPrompt() = + # if we return from this routine, user code executes again + var + again = True + dbgFramePtr = framePtr # for going down and up the stack + dbgDown = 0 # how often we did go down + dbgTemp: TStaticStr + + while again: + write(stdout, "*** endb| >>") + let oldLen = dbgUser.len + dbgUser.len = 0 + if not readLine(stdin, dbgUser): break + if dbgUser.len == 0: dbgUser.len = oldLen + # now look what we have to do: + var i = scanWord(dbgUser.data, dbgTemp, 0) + template `?`(x: expr): expr = dbgTemp == cstring(x) + if ?"s" or ?"step": + dbgState = dbStepInto + again = false + elif ?"n" or ?"next": + dbgState = dbStepOver + dbgSkipToFrame = framePtr + again = false + elif ?"f" or ?"skipcurrent": + dbgState = dbSkipCurrent + dbgSkipToFrame = framePtr.prev + again = false + elif ?"c" or ?"continue" or ?"r" or ?"run": + dbgState = dbBreakpoints + again = false + elif ?"i" or ?"ignore": + dbgState = dbOff + again = false + elif ?"h" or ?"help": + dbgHelp() + elif ?"q" or ?"quit": + dbgState = dbQuiting + dbgAborting = True + again = false + quit(1) # BUGFIX: quit with error code > 0 + elif ?"e" or ?"eval": + dbgEvaluate(stdout, dbgUser.data, i, dbgFramePtr) + elif ?"o" or ?"out": + dbgOut(dbgUser.data, i, dbgFramePtr) + elif ?"stackframe": + dbgStackFrame(dbgUser.data, i, dbgFramePtr) + elif ?"w" or ?"where": + dbgShowExecutionPoint() + elif ?"l" or ?"locals": + ListLocals(stdout, dbgFramePtr) + elif ?"g" or ?"globals": + ListGlobals(stdout) + elif ?"u" or ?"up": + if dbgDown <= 0: + debugOut("[Warning] cannot go up any further ") + else: + dbgFramePtr = framePtr + for j in 0 .. dbgDown-2: # BUGFIX + dbgFramePtr = dbgFramePtr.prev + dec(dbgDown) + dbgShowCurrentProc(dbgFramePtr) + elif ?"d" or ?"down": + if dbgFramePtr != nil: + inc(dbgDown) + dbgFramePtr = dbgFramePtr.prev + dbgShowCurrentProc(dbgFramePtr) + else: + debugOut("[Warning] cannot go down any further ") + elif ?"bt" or ?"backtrace": + dbgWriteStackTrace(framePtr) + elif ?"b" or ?"break": + createBreakPoint(dbgUser.data, i) + elif ?"breakpoints": + ListBreakPoints() + elif ?"toggle": + BreakpointToggle(dbgUser.data, i) + elif ?"filenames": + ListFilenames() + elif ?"maxdisplay": + var parsed: int + i = scanNumber(dbgUser.data, parsed, i) + if dbgUser.data[i-1] in {'0'..'9'}: + if parsed == 0: maxDisplayRecDepth = -1 + else: maxDisplayRecDepth = parsed + else: + InvalidCommand() + else: InvalidCommand() + +proc endbStep() = + # we get into here if an unhandled exception has been raised + # XXX: do not allow the user to run the program any further? + # XXX: BUG: the frame is lost here! + dbgShowExecutionPoint() + CommandPrompt() + +proc dbgWriteStackTrace(f: PFrame) = + const + firstCalls = 32 + var + it = f + i = 0 + total = 0 + tempFrames: array [0..127, PFrame] + # setup long head: + while it != nil and i <= high(tempFrames)-firstCalls: + tempFrames[i] = it + inc(i) + inc(total) + it = it.prev + # go up the stack to count 'total': + var b = it + while it != nil: + inc(total) + it = it.prev + var skipped = 0 + if total > len(tempFrames): + # skip N + skipped = total-i-firstCalls+1 + for j in 1..skipped: + if b != nil: b = b.prev + # create '...' entry: + tempFrames[i] = nil + inc(i) + # setup short tail: + while b != nil and i <= high(tempFrames): + tempFrames[i] = b + inc(i) + b = b.prev + for j in countdown(i-1, 0): + if tempFrames[j] == nil: + write(stdout, "(") + write(stdout, skipped) + write(stdout, " calls omitted) ...") + else: + write(stdout, tempFrames[j].filename) + if tempFrames[j].line > 0: + write(stdout, '(') + write(stdout, tempFrames[j].line) + write(stdout, ')') + write(stdout, ' ') + write(stdout, tempFrames[j].procname) + write(stdout, "\n") + +proc checkForBreakpoint = + let b = checkBreakpoints(framePtr.filename, framePtr.line) + if b != nil: + write(stdout, "*** endb| reached ") + write(stdout, framePtr.filename) + write(stdout, "(") + write(stdout, framePtr.line) + write(stdout, ") ") + write(stdout, framePtr.procname) + write(stdout, " ***\n") + CommandPrompt() + +proc lineHookImpl() {.nimcall.} = + case dbgState + of dbStepInto: + # we really want the command prompt here: + dbgShowExecutionPoint() + CommandPrompt() + of dbSkipCurrent, dbStepOver: # skip current routine + if framePtr == dbgSkipToFrame: + dbgShowExecutionPoint() + CommandPrompt() + else: + # breakpoints are wanted though (I guess) + checkForBreakpoint() + of dbBreakpoints: + # debugger is only interested in breakpoints + checkForBreakpoint() + else: nil + +proc watchpointHookImpl(name: cstring) {.nimcall.} = + dbgWriteStackTrace(framePtr) + debugOut(name) + +proc initDebugger {.inline.} = + dbgState = dbStepInto + dbgUser.len = 1 + dbgUser.data[0] = 's' + dbgWatchpointHook = watchpointHookImpl + dbgLineHook = lineHookImpl |