mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
- fixed duplicate detection - fixed parsing of breakpoint values >16bit (I misread the ParseInt() documentation) o disassembly / commands - GREP results now provide context o gopher2600 - initscript now definable on the command line o tia / playfield - implemented "scoremode" * I'd totally forgotten about this 2600 feature and was trying to understand why the "28c3 intro" demo wasn't showing the atari logo at the top of the screen. at the same time I had stumbled across SvOlli's Revision 2013 lecture where he mentions it. lucky. I would have spent a couple of hours probably, scratching my noggin.
280 lines
7.7 KiB
Go
280 lines
7.7 KiB
Go
// breakpoints are used to halt execution when a target is *changed to* a
|
|
// specific value. compare to traps which are used to halt execution when the
|
|
// target *changes from* its current value *to* any other value.
|
|
|
|
package debugger
|
|
|
|
import (
|
|
"fmt"
|
|
"gopher2600/debugger/input"
|
|
"gopher2600/debugger/ui"
|
|
"gopher2600/errors"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// breakpoints keeps track of all the currently defined breakers
|
|
type breakpoints struct {
|
|
dbg *Debugger
|
|
breaks []breaker
|
|
}
|
|
|
|
// breaker defines a specific break condition
|
|
type breaker struct {
|
|
target target
|
|
value interface{}
|
|
ignoreValue interface{}
|
|
|
|
// basic linked list to implement AND-conditions
|
|
next *breaker
|
|
prev *breaker
|
|
}
|
|
|
|
func (bk breaker) String() string {
|
|
b := strings.Builder{}
|
|
b.WriteString(fmt.Sprintf("%s->%s", bk.target.ShortLabel(), bk.target.FormatValue(bk.value)))
|
|
n := bk.next
|
|
for n != nil {
|
|
b.WriteString(fmt.Sprintf(" & %s->%s", n.target.ShortLabel(), bk.target.FormatValue(n.value)))
|
|
n = n.next
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// isSingleton checks if break condition is part of a list (false) or is a
|
|
// singleton condition (true)
|
|
func (bk breaker) isSingleton() bool {
|
|
return bk.next == nil && bk.prev == nil
|
|
}
|
|
|
|
// breaker.check checks the specific break condition with the current value of
|
|
// the break target
|
|
func (bk *breaker) check() bool {
|
|
currVal := bk.target.Value()
|
|
b := currVal == bk.value
|
|
|
|
if bk.next == nil {
|
|
b = b && currVal != bk.ignoreValue
|
|
|
|
// this is either a singleton break or the end of a break-list
|
|
// (inList==true). note how we set the ignoreValue in these two
|
|
// instances. if it's a singleton break then we always reset the
|
|
// ignoreValue. if it's the end of the list we reset the value to nil
|
|
// if there is no match
|
|
if bk.isSingleton() {
|
|
bk.ignoreValue = currVal
|
|
} else {
|
|
bk.ignoreValue = nil
|
|
}
|
|
return b
|
|
}
|
|
|
|
// this breaker is part of list so we need to recurse into the list
|
|
b = b && bk.next.check()
|
|
if b {
|
|
b = b && currVal != bk.ignoreValue
|
|
bk.ignoreValue = currVal
|
|
} else {
|
|
bk.ignoreValue = nil
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// add appends a new breaker object to the *end of the list* from the perspective
|
|
// of bk
|
|
func (bk *breaker) add(nbk *breaker) {
|
|
n := &bk.next
|
|
for *n != nil {
|
|
nbk.prev = *n
|
|
*n = (*n).next
|
|
}
|
|
*n = nbk
|
|
}
|
|
|
|
// newBreakpoints is the preferred method of initialisation for breakpoins
|
|
func newBreakpoints(dbg *Debugger) *breakpoints {
|
|
bp := new(breakpoints)
|
|
bp.dbg = dbg
|
|
bp.clear()
|
|
return bp
|
|
}
|
|
|
|
func (bp *breakpoints) clear() {
|
|
bp.breaks = make([]breaker, 0, 10)
|
|
}
|
|
|
|
func (bp *breakpoints) drop(num int) error {
|
|
if len(bp.breaks)-1 < num {
|
|
return errors.NewFormattedError(errors.CommandError, fmt.Errorf("breakpoint #%d is not defined", num))
|
|
}
|
|
|
|
h := bp.breaks[:num]
|
|
t := bp.breaks[num+1:]
|
|
bp.breaks = make([]breaker, len(h)+len(t), cap(bp.breaks))
|
|
copy(bp.breaks, h)
|
|
copy(bp.breaks[len(h):], t)
|
|
|
|
return nil
|
|
}
|
|
|
|
// breakpoints.check compares the current state of the emulation with every
|
|
// break condition. it returns a string listing every condition that applies
|
|
func (bp *breakpoints) check(previousResult string) string {
|
|
checkString := strings.Builder{}
|
|
checkString.WriteString(previousResult)
|
|
for i := range bp.breaks {
|
|
// check current value of target with the requested value
|
|
if bp.breaks[i].check() {
|
|
checkString.WriteString(fmt.Sprintf("break on %s\n", bp.breaks[i]))
|
|
}
|
|
}
|
|
return checkString.String()
|
|
}
|
|
|
|
func (bp breakpoints) list() {
|
|
if len(bp.breaks) == 0 {
|
|
bp.dbg.print(ui.Feedback, "no breakpoints")
|
|
} else {
|
|
for i := range bp.breaks {
|
|
bp.dbg.print(ui.Feedback, "% 2d: %s", i, bp.breaks[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseBreakpoints consumes tokens and adds new conditions to the list of
|
|
// breakpoints. For example:
|
|
//
|
|
// PC 0xf000
|
|
// adds a new breakpoint to the PC
|
|
//
|
|
// 0xf000
|
|
// is the same, because we assume a target of PC if none is given
|
|
//
|
|
// X 10 11
|
|
// adds two new breakpoints to X - we've changed targets so the second value
|
|
// is assumed to be for the previously selected target
|
|
//
|
|
// X 10 11 Y 12
|
|
// add three breakpoints; 2 to X and 1 to Y
|
|
//
|
|
// SL 100 & HP 0
|
|
// add one AND-condition
|
|
//
|
|
// SL 100 & HP 0 | X 10
|
|
// add two conditions; one AND-condition and one condition on X
|
|
//
|
|
// note that this is a very simple parser and we can do unusual things: the &
|
|
// and | symbols simply switch "modes", with unusual consequences. for example,
|
|
// the last example above could be written:
|
|
//
|
|
// & SL 100 HP 0 | X 10
|
|
//
|
|
// TODO: more sophisticated breakpoints parser
|
|
func (bp *breakpoints) parseBreakpoint(tokens *input.Tokens) error {
|
|
andBreaks := false
|
|
|
|
// default target of CPU PC. meaning that "BREAK n" will cause a breakpoint
|
|
// being set on the PC. breaking on PC is probably the most common type of
|
|
// breakpoint. the target will change value when the input string sees
|
|
// something appropriate
|
|
tgt := target(bp.dbg.vcs.MC.PC)
|
|
|
|
// resolvedTarget keeps track of whether we have specified a target but not
|
|
// given any values for that target. we set it to true initially because
|
|
// we want to be able to change the default target
|
|
resolvedTarget := true
|
|
|
|
// we don't add new breakpoints to the main list straight away. we append
|
|
// them to newBreaks first and then check that we aren't adding duplicates
|
|
newBreaks := make([]breaker, 0, 10)
|
|
|
|
// loop over tokens. if token is a number then add the breakpoint for the
|
|
// current target. if it is not a number, look for a keyword that changes
|
|
// the target (or run a BREAK meta-command)
|
|
tok, present := tokens.Get()
|
|
for present {
|
|
// if token is a number...
|
|
val, err := strconv.ParseInt(tok, 0, 32)
|
|
if err == nil {
|
|
if andBreaks == true {
|
|
if len(newBreaks) == 0 {
|
|
newBreaks = append(newBreaks, breaker{target: tgt, value: int(val)})
|
|
} else {
|
|
newBreaks[len(newBreaks)-1].add(&breaker{target: tgt, value: int(val)})
|
|
}
|
|
resolvedTarget = true
|
|
} else {
|
|
newBreaks = append(newBreaks, breaker{target: tgt, value: int(val)})
|
|
resolvedTarget = true
|
|
}
|
|
|
|
} else {
|
|
// if token is not a number ...
|
|
|
|
// make sure we've not left a previous target dangling without a value
|
|
if !resolvedTarget {
|
|
return errors.NewFormattedError(errors.CommandError, fmt.Errorf("need a value to break on (%s)", tgt.Label()))
|
|
}
|
|
|
|
// possibly switch composition mode
|
|
if tok == "&" {
|
|
andBreaks = true
|
|
} else if tok == "|" {
|
|
andBreaks = false
|
|
} else {
|
|
// token is not a number or a composition symbol so try to
|
|
// parse a new target
|
|
tokens.Unget()
|
|
tgt, err = parseTarget(bp.dbg, tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolvedTarget = false
|
|
}
|
|
}
|
|
|
|
tok, present = tokens.Get()
|
|
}
|
|
|
|
if !resolvedTarget {
|
|
return errors.NewFormattedError(errors.CommandError, fmt.Errorf("need a value to break on (%s)", tgt.Label()))
|
|
}
|
|
|
|
return bp.checkNewBreakpoints(newBreaks)
|
|
}
|
|
|
|
func (bp *breakpoints) checkNewBreakpoints(newBreaks []breaker) error {
|
|
// don't add breakpoints that already exist
|
|
for _, nb := range newBreaks {
|
|
for _, ob := range bp.breaks {
|
|
and := &nb
|
|
oand := &ob
|
|
|
|
// start with assuming this is a duplicate
|
|
duplicate := true
|
|
|
|
// continue comparison until we reach the end of one of the lists
|
|
// or if a non-duplicate condition has been found
|
|
for duplicate && and != nil && oand != nil {
|
|
// note that this method of duplication detection only works if
|
|
// targets are ANDed in the same order.
|
|
//
|
|
// TODO: sort conditions before comparison
|
|
duplicate = duplicate && (oand.target.Label() == and.target.Label() && oand.value == and.value)
|
|
|
|
and = and.next
|
|
oand = oand.next
|
|
}
|
|
|
|
// fail if this is a duplicate and if both lists were of the same length
|
|
if duplicate && and == nil && oand == nil {
|
|
return errors.NewFormattedError(errors.CommandError, fmt.Errorf("breakpoint already exists (%s)", ob))
|
|
}
|
|
}
|
|
|
|
bp.breaks = append(bp.breaks, nb)
|
|
}
|
|
|
|
return nil
|
|
}
|