Gopher2600/debugger/halt_targets.go
JetSetIlly 669ff5bdad stepping over WSYNC is correctly implemented again
I think originally, after the debugging loop refactor, I was planning on
implementing this as STEP OVER and allowing a regular STEP (even in
instruction quantum) to advance to the next clock. but it proved to be
annoying and confusing
2021-11-21 07:41:13 +00:00

282 lines
7.7 KiB
Go

// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package debugger
import (
"fmt"
"strings"
"github.com/jetsetilly/gopher2600/curated"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
)
// targetValue represents the underlying value of the target. for example in
// the case of the CPU program counter target, the underlying type is a uint16.
//
// !!TODO: candidate for generic / comparable type
type targetValue interface{}
type target struct {
// a label for the label
label string
// the current value of the target. note this may not be the same value as the
// underlying target. for example, target PC will return zero if there is a
// coprocessor running.
value func() targetValue
// the value returned by the value field/function can be formatted for
// presentation purposes with formatValue()
format string
// some targets should only be checked on an instruction boundary
instructionBoundary bool
// this target will always break in playmode almost immediately. we use
// this flag to decide whether to allow the debugger to switch to playmode
notInPlaymode bool
}
// returns value() formated by the format string. accepts a target value as an
// argument so the format string can be used on any valid targetValue.
func (trg target) stringValue(v targetValue) string {
if trg.format == "" {
return fmt.Sprintf("%v", v)
}
return fmt.Sprintf(trg.format, v)
}
// parseTarget interprets the next token and returns a target if it is
// recognised. returns error if it is not.
func parseTarget(dbg *Debugger, tokens *commandline.Tokens) (*target, error) {
var trg *target
keyword, present := tokens.Get()
if present {
keyword = strings.ToUpper(keyword)
switch keyword {
// cpu registers
case "PC":
trg = &target{
label: "PC",
value: func() targetValue {
// check that there is no coprocessor running - the ARM
// class of coprocessors cause the 6507 to run NOP
// instructions, which causes the PC to advance. this means
// that a PC break can be triggered when the user probably
// doesn't want it to be.
bank := dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
if bank.ExecutingCoprocessor {
// we return zero. it's highly unlikely a genuine BREAK PC 0
// has been requested but even so, this isn't great design.
// better to return two values perhaps, the second value saying
// that the target is out of action.
return 0
}
// for breakpoints it is important that the breakpoint
// value be normalised through MapAddress() too
ai := dbg.dbgmem.MapAddress(dbg.vcs.CPU.PC.Address(), true)
return int(ai.MappedAddress)
},
format: "%#04x",
instructionBoundary: true,
}
case "A":
trg = &target{
label: "A",
value: func() targetValue {
return int(dbg.vcs.CPU.A.Value())
},
format: "%#02x",
instructionBoundary: true,
}
case "X":
trg = &target{
label: "X",
value: func() targetValue {
return int(dbg.vcs.CPU.X.Value())
},
format: "%#02x",
instructionBoundary: true,
}
case "Y":
trg = &target{
label: "Y",
value: func() targetValue {
return int(dbg.vcs.CPU.Y.Value())
},
format: "%#02x",
instructionBoundary: true,
}
case "SP":
trg = &target{
label: "SP",
value: func() targetValue {
return int(dbg.vcs.CPU.SP.Value())
},
format: "%#02x",
instructionBoundary: true,
}
case "RDY":
trg = &target{
label: "RDY",
value: func() targetValue {
return bool(dbg.vcs.CPU.RdyFlg)
},
format: "%v",
instructionBoundary: true,
}
// tv state
case "FRAMENUM", "FRAME", "FR":
trg = &target{
label: "Frame",
value: func() targetValue {
return dbg.vcs.TV.GetCoords().Frame
},
}
case "SCANLINE", "SL":
trg = &target{
label: "Scanline",
value: func() targetValue {
return dbg.vcs.TV.GetCoords().Scanline
},
// specifying scanline to be notInPlaymode was considered but
// this will occasionaly not be true. for example, a scanline
// value that is beyond the extent of the generated TV frame
}
case "CLOCK", "CL":
trg = &target{
label: "Clock",
value: func() targetValue {
return dbg.vcs.TV.GetCoords().Clock
},
// it is impossible to measure the clock value accurately in
// playmode because the state of the machine is only checked
// after every CPU instruction
//
// another reason not to allow playmode when this halt target
// is being used, is that the emulation will almost immediately
// halt - the clock value *will* be reached within 228 ticks of
// the emulation
notInPlaymode: true,
}
case "BANK":
trg = bankTarget(dbg)
// cpu instruction targeting was originally added as an experiment, to
// help investigate a bug in the emulation. I don't think it's much use
// but it was an instructive exercise and may come in useful one day.
case "RESULT", "RES":
subkey, present := tokens.Get()
if present {
subkey = strings.ToUpper(subkey)
switch subkey {
case "OPERATOR", "OP":
trg = &target{
label: "Operator",
value: func() targetValue {
if !dbg.vcs.CPU.LastResult.Final || dbg.vcs.CPU.LastResult.Defn == nil {
return ""
}
return dbg.vcs.CPU.LastResult.Defn.Operator
},
instructionBoundary: true,
}
case "ADDRESSMODE", "AM":
trg = &target{
label: "AddressMode",
value: func() targetValue {
if !dbg.vcs.CPU.LastResult.Final || dbg.vcs.CPU.LastResult.Defn == nil {
return ""
}
return int(dbg.vcs.CPU.LastResult.Defn.AddressingMode)
},
instructionBoundary: true,
}
case "EFFECT", "EFF":
trg = &target{
label: "Instruction Effect",
value: func() targetValue {
if !dbg.vcs.CPU.LastResult.Final {
return -1
}
return int(dbg.vcs.CPU.LastResult.Defn.Effect)
},
instructionBoundary: true,
}
case "PAGEFAULT", "PAGE":
trg = &target{
label: "PageFault",
value: func() targetValue {
return dbg.vcs.CPU.LastResult.PageFault
},
instructionBoundary: true,
}
case "BUG":
trg = &target{
label: "CPU Bug",
value: func() targetValue {
s := dbg.vcs.CPU.LastResult.CPUBug
if s == "" {
return "ok"
}
return s
},
instructionBoundary: true,
}
default:
return nil, curated.Errorf("invalid target: %s %s", keyword, subkey)
}
} else {
return nil, curated.Errorf("invalid target: %s", keyword)
}
default:
return nil, curated.Errorf("invalid target: %s", keyword)
}
}
return trg, nil
}
// a bank target is generated automatically by the breakpoints system and also
// explicitly by parseTarget()
func bankTarget(dbg *Debugger) *target {
return &target{
label: "Bank",
value: func() targetValue {
return dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address()).Number
},
instructionBoundary: true,
}
}