Gopher2600/debugger/commands.go
JetSetIlly d32262adff simplified how gui implements and handles notifications
debugger no longer sends play, pause notifications to the gui. the gui
polls for that information as required

govern package now has SubState type to complement the State type.
StateIntegrity() function enforces combinations of State and SubState,
called from debugger.setState() function

playmode notifications reworked and contained in a single playmode_overlay.go
file. this includes the FPS and screen detail

preference value sdlimgui.playmode.fpsOverlay replaced with
sdlimgui.playmode.fpsDetail. still toggled with F7 key

coproc icon moved to top-left corner of playmode overlay and only
visible when FPS detail is showing

when FPS detail is showing multiple (small) icons care shown. when it is
not showing, a single (large) icon is shown according to the priority of
the icon. eg. pause indicator has higher priority than the mute
indicator
2024-04-12 18:20:29 +01:00

2158 lines
58 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 (
"encoding/hex"
"fmt"
"io"
"os"
"runtime"
"sort"
"strconv"
"strings"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/coprocessor"
coproc_breakpoints "github.com/jetsetilly/gopher2600/coprocessor/developer/breakpoints"
"github.com/jetsetilly/gopher2600/coprocessor/developer/callstack"
"github.com/jetsetilly/gopher2600/coprocessor/developer/dwarf"
"github.com/jetsetilly/gopher2600/coprocessor/developer/faults"
"github.com/jetsetilly/gopher2600/coprocessor/developer/yield"
"github.com/jetsetilly/gopher2600/debugger/dbgmem"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/debugger/script"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/disassembly"
"github.com/jetsetilly/gopher2600/disassembly/symbols"
"github.com/jetsetilly/gopher2600/gui"
"github.com/jetsetilly/gopher2600/hardware/cpu/registers"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/plusrom"
"github.com/jetsetilly/gopher2600/hardware/memory/memorymap"
"github.com/jetsetilly/gopher2600/hardware/peripherals/atarivox"
"github.com/jetsetilly/gopher2600/hardware/peripherals/controllers"
"github.com/jetsetilly/gopher2600/hardware/peripherals/savekey"
"github.com/jetsetilly/gopher2600/hardware/riot/ports"
"github.com/jetsetilly/gopher2600/hardware/riot/ports/plugging"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
"github.com/jetsetilly/gopher2600/logger"
"github.com/jetsetilly/gopher2600/patch"
"github.com/jetsetilly/gopher2600/resources/unique"
"github.com/jetsetilly/gopher2600/version"
)
var debuggerCommands *commandline.Commands
var scriptUnsafeCommands *commandline.Commands
// this init() function "compiles" the commandTemplate above into a more
// usuable form. It will cause the program to fail if the template is invalid.
func init() {
var err error
// parse command template
debuggerCommands, err = commandline.ParseCommandTemplate(commandTemplate)
if err != nil {
panic(err)
}
err = debuggerCommands.AddHelp(cmdHelp, helps)
if err != nil {
panic(err)
}
sort.Stable(debuggerCommands)
scriptUnsafeCommands, err = commandline.ParseCommandTemplate(scriptUnsafeTemplate)
if err != nil {
panic(err)
}
sort.Stable(scriptUnsafeCommands)
}
// parseCommand tokenises the input and processes the tokens.
func (dbg *Debugger) parseCommand(cmd string, scribe bool, echo bool) error {
tokens, err := dbg.tokeniseCommand(cmd, scribe, echo)
if err != nil {
return err
}
return dbg.processTokens(tokens)
}
// return tokenised command.
func (dbg *Debugger) tokeniseCommand(cmd string, scribe bool, echo bool) (*commandline.Tokens, error) {
// tokenise input
tokens := commandline.TokeniseInput(cmd)
// if there are no tokens in the input then continue with a default action
if tokens.Remaining() == 0 {
return dbg.tokeniseCommand("STEP", true, false)
}
// check validity of tokenised input
err := debuggerCommands.ValidateTokens(tokens)
if err != nil {
return nil, err
}
// print normalised input if this is command from an interactive source
// and not an auto-command
if echo {
dbg.printLine(terminal.StyleEcho, tokens.String())
}
// test to see if command is allowed when recording/playing a script
if dbg.scriptScribe.IsActive() && scribe {
tokens.Reset()
err := scriptUnsafeCommands.ValidateTokens(tokens)
// fail when the tokens DO match the scriptUnsafe template (ie. when
// there is no err from the validate function)
if err == nil {
return nil, fmt.Errorf("'%s' is unsafe to use in scripts", tokens.String())
}
// record command if it auto is false (is not a result of an "auto" command
// eg. ONHALT). if there's an error then the script will be rolled back and
// the write removed.
dbg.scriptScribe.WriteInput(tokens.String())
}
return tokens, nil
}
// processTokensList call processTokens for each entry in the array of tokens.
// this is useful when we have already parsed and tokenised command input and
// simply want to rerun those commands.
//
// used by the ONSTEP, ONHALT and ONTRACE features.
func (dbg *Debugger) processTokensList(tokenGrp []*commandline.Tokens) error {
var err error
for _, t := range tokenGrp {
err = dbg.processTokens(t)
if err != nil {
return err
}
}
return nil
}
// process a single command (with arguments).
func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
// check first token. if this token makes sense then we will consume the
// rest of the tokens appropriately
tokens.Reset()
command, _ := tokens.Get()
switch command {
default:
return fmt.Errorf("%s is not yet implemented", command)
case cmdHelp:
keyword, ok := tokens.Get()
if ok {
dbg.printLine(terminal.StyleHelp, debuggerCommands.Help(keyword))
} else {
dbg.printLine(terminal.StyleHelp, debuggerCommands.HelpOverview())
}
// we don't want the HELP command to appear in the script
dbg.scriptScribe.Rollback()
return nil
case cmdQuit:
if dbg.scriptScribe.IsActive() {
dbg.printLine(terminal.StyleFeedback, "ending script recording")
// we don't want the QUIT command to appear in the script
dbg.scriptScribe.Rollback()
return dbg.scriptScribe.EndSession()
} else {
dbg.running = false
}
case cmdReset:
// resetting in the middle of a CPU instruction requires the input loop
// to be unwound before continuing
dbg.unwindLoop(func() error {
// don't reset breakpoints, etc.
err := dbg.reset(false)
if err != nil {
return err
}
dbg.printLine(terminal.StyleFeedback, "machine reset")
return nil
})
case cmdRun:
dbg.runUntilHalt = true
dbg.continueEmulation = true
return nil
case cmdHalt:
dbg.haltImmediately = true
case cmdStep:
adjAmount := 1
back := false
if tk, ok := tokens.Get(); ok {
switch tk {
case "BACK":
back = true
adjAmount *= -1
case "OVER":
// if next expected opcode is JSR then add a volatile breakpoint to the return address
//
// at the time of writing this comment, if execution is currently in RAM then
// GetEntryByAddress() will return nil. this means that if "JSR" is encounterd during
// that time, the STEP OVER command will not work as expected
e := dbg.Disasm.GetEntryByAddress(dbg.vcs.CPU.PC.Address())
if e != nil && e.Operator == "jsr" {
brk := commandline.TokeniseInput(fmt.Sprintf("%#4x", dbg.vcs.CPU.PC.Address()+3))
dbg.halting.volatileBreakpoints.parseCommand(brk)
// breakpoing will take many cycles to trigger so we need to run the emulation
dbg.runUntilHalt = true
}
default:
tokens.Unget()
}
}
// get mode
mode, _ := tokens.Get()
mode = strings.ToUpper(mode)
if back {
// step backwards
var coords coords.TelevisionCoords
switch mode {
case "":
// use current quantum state
switch dbg.Quantum() {
case govern.QuantumInstruction:
coords = dbg.cpuBoundaryLastInstruction
case govern.QuantumCycle:
coords = dbg.vcs.TV.AdjCoords(television.AdjCycle, adjAmount)
case govern.QuantumClock:
coords = dbg.vcs.TV.AdjCoords(television.AdjClock, adjAmount)
}
case "INSTRUCTION":
dbg.setQuantum(govern.QuantumInstruction)
coords = dbg.cpuBoundaryLastInstruction
case "CYCLE":
dbg.setQuantum(govern.QuantumCycle)
coords = dbg.vcs.TV.AdjCoords(television.AdjCycle, adjAmount)
case "CLOCK":
dbg.setQuantum(govern.QuantumClock)
coords = dbg.vcs.TV.AdjCoords(television.AdjClock, adjAmount)
case "SCANLINE":
coords = dbg.vcs.TV.AdjCoords(television.AdjScanline, adjAmount)
case "FRAME":
coords = dbg.vcs.TV.AdjCoords(television.AdjFrame, adjAmount)
default:
return fmt.Errorf("unknown STEP BACK mode (%s)", mode)
}
dbg.setState(govern.Rewinding, govern.RewindingBackwards)
dbg.unwindLoop(func() error {
dbg.catchupContext = catchupStepBack
return dbg.Rewind.GotoCoords(coords)
})
} else {
// step forwards
switch mode {
case "":
// continue with current quantum state
// if quantum is not the QuantumClock and CPU is not RDY then STEP
// is best implemented as TRAP RDY. this means that the emulation
// will stop on the next instruction boundary and will also skip
// over instructions that trigger WSYNC
//
// this behaviour is more intuitive to the user because it means
// they don't have to step over every cycle during the WSYNC state
if dbg.Quantum() != govern.QuantumClock && !dbg.vcs.CPU.RdyFlg {
// create volatile RDY trap
_ = dbg.halting.volatileTraps.parseCommand(commandline.TokeniseInput("RDY"))
dbg.runUntilHalt = true
// when the RDY flag changes the input loop will think it's
// inside a video step. we need to force the loop to return
// to the non-video step loop
dbg.stepOutOfVideoStepInputLoop = true
}
case "INSTRUCTION":
dbg.setQuantum(govern.QuantumInstruction)
case "CYCLE":
dbg.setQuantum(govern.QuantumCycle)
case "CLOCK":
dbg.setQuantum(govern.QuantumClock)
default:
// token not recognised so forward rest of tokens to the volatile
// traps parser
tokens.Unget()
_ = dbg.halting.volatileTraps.parseCommand(tokens)
// trap may take many cycles to trigger
dbg.runUntilHalt = true
}
// continue emulation. note that we don't set runUntilHalt except in the
// specific cases above in the above switch. this is because we do no
// always set a volatile trap. without a trap the emulation will just
// run until it receives a HALT instruction.
dbg.continueEmulation = true
}
case cmdQuantum:
mode, _ := tokens.Get()
mode = strings.ToUpper(mode)
switch mode {
case "INSTRUCTION":
dbg.setQuantum(govern.QuantumInstruction)
case "CYCLE":
dbg.setQuantum(govern.QuantumCycle)
case "CLOCK":
dbg.setQuantum(govern.QuantumClock)
default:
dbg.printLine(terminal.StyleFeedback, "set to %s", strings.ToUpper(dbg.Quantum().String()))
}
case cmdScript:
option, _ := tokens.Get()
switch strings.ToUpper(option) {
case "RECORD":
var err error
saveFile, _ := tokens.Get()
err = dbg.scriptScribe.StartSession(saveFile)
if err != nil {
return err
}
// we don't want SCRIPT RECORD command to appear in the script
dbg.scriptScribe.Rollback()
return nil
case "END":
// we don't want SCRIPT END command to appear in the script
dbg.scriptScribe.Rollback()
return dbg.scriptScribe.EndSession()
default:
// run a script
scr, err := script.RescribeScript(option)
if err != nil {
return err
}
if dbg.scriptScribe.IsActive() {
// if we're currently recording a script we want to write this
// command to the new script file but indicate that we'll be
// entering a new script and so don't want to repeat the
// commands from that script
err := dbg.scriptScribe.StartPlayback()
if err != nil {
return err
}
defer dbg.scriptScribe.EndPlayback()
}
err = dbg.inputLoop(scr, false)
if err != nil {
return err
}
}
case cmdRewind:
// note that we calling the rewind.GotoFrame() functions directly and not
// using the debugger.PushRewind() function.
arg, ok := tokens.Get()
if ok {
// stop emulation on rewind
dbg.runUntilHalt = false
if arg == "LAST" {
dbg.setState(govern.Rewinding, govern.RewindingForwards)
dbg.unwindLoop(dbg.Rewind.GotoLast)
} else if arg == "SUMMARY" {
dbg.printLine(terminal.StyleInstrument, dbg.Rewind.Peephole())
} else {
frame, _ := strconv.Atoi(arg)
coords := dbg.TV().GetCoords()
if frame != coords.Frame {
if frame < coords.Frame {
dbg.setState(govern.Rewinding, govern.RewindingBackwards)
} else {
dbg.setState(govern.Rewinding, govern.RewindingForwards)
}
dbg.unwindLoop(func() error {
err := dbg.Rewind.GotoFrame(frame)
if err != nil {
return err
}
return nil
})
}
}
return nil
}
case cmdComparison:
arg, ok := tokens.Get()
if ok {
switch arg {
case "LOCK":
dbg.Rewind.LockComparison(true)
case "UNLOCK":
dbg.Rewind.LockComparison(false)
if dbg.State() == govern.Running {
dbg.Rewind.UpdateComparison()
}
default:
frame, _ := strconv.Atoi(arg)
dbg.Rewind.SetComparison(frame)
}
}
case cmdGoto:
fromCoords := dbg.vcs.TV.GetCoords()
toCoords := fromCoords
if s, ok := tokens.Get(); ok {
toCoords.Clock, _ = strconv.Atoi(s)
if s, ok := tokens.Get(); ok {
toCoords.Scanline, _ = strconv.Atoi(s)
if s, ok := tokens.Get(); ok {
toCoords.Frame, _ = strconv.Atoi(s)
}
}
}
if coords.GreaterThan(toCoords, fromCoords) {
dbg.setState(govern.Rewinding, govern.RewindingForwards)
} else {
dbg.setState(govern.Rewinding, govern.RewindingBackwards)
}
dbg.unwindLoop(func() error {
err := dbg.Rewind.GotoCoords(toCoords)
if err != nil {
return err
}
return nil
})
case cmdInsert:
dbg.unwindLoop(func() error {
cart, _ := tokens.Get()
cl, err := cartridgeloader.NewLoader(cart, "AUTO")
if err != nil {
return err
}
err = dbg.attachCartridge(cl)
if err != nil {
return err
}
// use cartridge's idea of the filename. the attach process may have
// caused a different cartridge to load than the one requested (most
// typically this will mean that the cartridge has been ejected)
dbg.printLine(terminal.StyleFeedback, "machine reset with new cartridge (%s)", dbg.vcs.Mem.Cart.Filename)
return nil
})
case cmdCartridge:
arg, ok := tokens.Get()
if ok {
switch arg {
case "PATH":
dbg.printLine(
terminal.StyleInstrument,
dbg.vcs.Mem.Cart.Filename,
)
case "NAME":
dbg.printLine(
terminal.StyleInstrument,
dbg.vcs.Mem.Cart.ShortName,
)
case "MAPPER":
dbg.printLine(
terminal.StyleInstrument,
dbg.vcs.Mem.Cart.ID(),
)
case "CONTAINER":
dbg.printLine(
terminal.StyleInstrument,
dbg.vcs.Mem.Cart.ContainerID(),
)
case "MAPPEDBANKS":
dbg.printLine(
terminal.StyleInstrument,
dbg.vcs.Mem.Cart.MappedBanks(),
)
case "HASH":
dbg.printLine(
terminal.StyleFeedback,
dbg.vcs.Mem.Cart.Hash,
)
case "STATIC":
// !!TODO: poke/peek static cartridge static data areas
if bus := dbg.vcs.Mem.Cart.GetStaticBus(); bus != nil {
static := bus.GetStatic()
if static != nil {
dbg.printLine(terminal.StyleFeedback, "cartridge has a static data area")
} else {
dbg.printLine(terminal.StyleFeedback, "cartridge has no static data area")
}
} else {
dbg.printLine(terminal.StyleFeedback, "cartridge has no static data area")
}
case "REGISTERS":
// !!TODO: poke/peek cartridge registers
if bus := dbg.vcs.Mem.Cart.GetRegistersBus(); bus != nil {
dbg.printLine(terminal.StyleInstrument, bus.GetRegisters().String())
} else {
dbg.printLine(terminal.StyleFeedback, "cartridge has no registers")
}
case "RAM":
// cartridge RAM is accessible through the normal VCS buses so
// the normal peek/poke commands will work
if bus := dbg.vcs.Mem.Cart.GetRAMbus(); bus != nil {
ram := bus.GetRAM()
if ram != nil {
s := &strings.Builder{}
for b := 0; b < len(ram); b++ {
s.WriteString(ram[b].Label + "\n")
s.WriteString(strings.Repeat("-", len(ram[b].Label)))
s.WriteString("\n")
s.WriteString(hex.Dump(ram[b].Data))
s.WriteString("\n\n")
}
dbg.printLine(terminal.StyleInstrument, s.String())
} else {
dbg.printLine(terminal.StyleFeedback, "cartridge has no RAM")
}
} else {
dbg.printLine(terminal.StyleFeedback, "cartridge has no RAM")
}
case "HOTLOAD":
err := dbg.hotload()
if err != nil {
dbg.printLine(terminal.StyleFeedback, err.Error())
} else {
dbg.printLine(terminal.StyleFeedback, "hotload successful")
}
case "DUMP":
romdump, err := dbg.vcs.Mem.Cart.ROMDump()
if err != nil {
dbg.printLine(terminal.StyleFeedback, err.Error())
} else {
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("rom dumped to %s", romdump))
}
}
} else {
dbg.printLine(terminal.StyleInstrument, dbg.vcs.Mem.Cart.String())
}
case cmdPatch:
f, _ := tokens.Get()
patched, err := patch.CartridgeMemory(dbg.vcs.Mem.Cart, f)
if err != nil {
dbg.printLine(terminal.StyleError, "%v", err)
if patched {
dbg.printLine(terminal.StyleError, "error during patching. cartridge might be unusable.")
}
return nil
}
if patched {
dbg.printLine(terminal.StyleFeedback, "cartridge patched")
}
case cmdDisasm:
bytecode := false
arg, ok := tokens.Get()
if ok {
switch arg {
case "REDUX":
err := dbg.Disasm.FromMemory()
if err != nil {
dbg.printLine(terminal.StyleFeedback, err.Error())
}
return nil
case "BYTECODE":
bytecode = true
}
}
attr := disassembly.ColumnAttr{
ByteCode: bytecode,
Label: true,
Cycles: true,
}
s := strings.Builder{}
err := dbg.Disasm.Write(&s, attr)
if err != nil {
dbg.printLine(terminal.StyleFeedback, err.Error())
}
dbg.printLine(terminal.StyleFeedback, s.String())
case cmdGrep:
scope := disassembly.GrepAll
output := &strings.Builder{}
s, _ := tokens.Get()
switch strings.ToUpper(s) {
case "COPROC":
search, _ := tokens.Get()
addr, err := strconv.ParseUint(search, 0, 32)
if err != nil {
dbg.printLine(terminal.StyleError, "search term for COPROC must be a 32bit address")
return nil
}
var ln *dwarf.SourceLine
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source files found")
return
}
var ok bool
ln, ok = src.LinesByAddress[uint64(addr)]
if !ok {
dbg.printLine(terminal.StyleError, fmt.Sprintf("address %x does not correspond to a source line", addr))
} else {
dbg.printLine(terminal.StyleFeedback, ln.String())
}
})
_ = dbg.gui.SetFeature(gui.ReqCoProcSourceLine, ln)
return nil
case "OPERATOR":
scope = disassembly.GrepOperator
case "OPERAND":
scope = disassembly.GrepOperand
default:
tokens.Unget()
}
search, _ := tokens.Get()
err := dbg.Disasm.Grep(output, scope, search, false)
if err != nil {
return err
}
if output.Len() == 0 {
dbg.printLine(terminal.StyleError, "%s not found in disassembly", search)
} else {
dbg.printLine(terminal.StyleFeedback, output.String())
}
case cmdSymbol:
tok, _ := tokens.Get()
switch strings.ToUpper(tok) {
case "LIST":
option, ok := tokens.Get()
if ok {
switch strings.ToUpper(option) {
default:
// already caught by command line ValidateTokens()
case "LABELS":
dbg.dbgmem.Sym.ListLabels(dbg.writerInStyle(terminal.StyleFeedback))
case "READ":
dbg.dbgmem.Sym.ListReadSymbols(dbg.writerInStyle(terminal.StyleFeedback))
case "WRITE":
dbg.dbgmem.Sym.ListWriteSymbols(dbg.writerInStyle(terminal.StyleFeedback))
}
} else {
dbg.dbgmem.Sym.ListSymbols(dbg.writerInStyle(terminal.StyleFeedback))
}
default:
symbol := tok
symSearch := dbg.Disasm.Sym.SearchBySymbol(symbol, symbols.SearchLabel)
if symSearch != nil {
ai := dbg.dbgmem.GetAddressInfo(symSearch.Address, true)
if ai != nil {
dbg.printLine(terminal.StyleFeedback, "%s [LABEL]", ai.String())
} else {
symSearch = nil
}
}
aiRead := dbg.dbgmem.GetAddressInfo(symbol, true)
if aiRead != nil {
dbg.printLine(terminal.StyleFeedback, "%s [READ]", aiRead.String())
}
aiWrite := dbg.dbgmem.GetAddressInfo(symbol, false)
if aiWrite != nil {
dbg.printLine(terminal.StyleFeedback, "%s [WRITE]", aiWrite.String())
}
if symSearch == nil && aiRead == nil && aiWrite == nil {
dbg.printLine(terminal.StyleFeedback, "%s not found in any symbol table", symbol)
}
}
case cmdOnHalt:
if tokens.Remaining() == 0 {
if len(dbg.commandOnHalt) == 0 {
dbg.printLine(terminal.StyleFeedback, "auto-command on halt: OFF")
} else {
s := strings.Builder{}
for _, c := range dbg.commandOnHalt {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on halt: %s", strings.TrimSuffix(s.String(), "; "))
}
return nil
}
var input string
option, _ := tokens.Get()
switch strings.ToUpper(option) {
case "OFF":
dbg.commandOnHalt = dbg.commandOnHalt[:0]
dbg.printLine(terminal.StyleFeedback, "no command on halt")
return nil
case "ON":
dbg.commandOnHalt = dbg.commandOnHaltStored
for _, c := range dbg.commandOnHalt {
dbg.printLine(terminal.StyleFeedback, "auto-command on halt: %s", c)
}
return nil
default:
// token isn't one we recognise so push it back onto the token queue
tokens.Unget()
// use remaininder of command line to form the ONHALT command sequence
input = strings.TrimSpace(tokens.Remainder())
tokens.End()
}
// empty list of tokens. taking note of existing command - not the same
// as commandOnHaltStored because ONHALT might be OFF
existingOnHalt := dbg.commandOnHalt
dbg.commandOnHalt = dbg.commandOnHalt[:0]
// tokenise commands to check for integrity
for _, s := range strings.Split(input, ",") {
toks, err := dbg.tokeniseCommand(s, false, false)
if err != nil {
dbg.commandOnHalt = existingOnHalt
return err
}
dbg.commandOnHalt = append(dbg.commandOnHalt, toks)
}
// make a copy of
dbg.commandOnHaltStored = dbg.commandOnHalt
// display the new ONHALT command(s)
s := strings.Builder{}
for _, c := range dbg.commandOnHalt {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on halt: %s", strings.TrimSuffix(s.String(), "; "))
return nil
case cmdOnStep:
if tokens.Remaining() == 0 {
if len(dbg.commandOnStep) == 0 {
dbg.printLine(terminal.StyleFeedback, "no command on step")
} else {
s := strings.Builder{}
for _, c := range dbg.commandOnStep {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on step: %s", strings.TrimSuffix(s.String(), "; "))
}
return nil
}
var input string
option, _ := tokens.Get()
switch strings.ToUpper(option) {
case "OFF":
dbg.commandOnStep = dbg.commandOnStep[:0]
dbg.printLine(terminal.StyleFeedback, "auto-command on step: OFF")
return nil
case "ON":
dbg.commandOnStep = dbg.commandOnStepStored
for _, c := range dbg.commandOnStep {
dbg.printLine(terminal.StyleFeedback, "auto-command on step: %s", c)
}
return nil
default:
// token isn't one we recognise so push it back onto the token queue
tokens.Unget()
// use remaininder of command line to form the ONSTEP command sequence
input = strings.TrimSpace(tokens.Remainder())
tokens.End()
}
// empty list of tokens. taking note of existing command - not the same
// as commandOnStepStored because ONSTEP might be OFF
existingOnStep := dbg.commandOnStep
dbg.commandOnStep = dbg.commandOnStep[:0]
// tokenise commands to check for integrity
for _, s := range strings.Split(input, ",") {
toks, err := dbg.tokeniseCommand(s, false, false)
if err != nil {
dbg.commandOnStep = existingOnStep
return err
}
dbg.commandOnStep = append(dbg.commandOnStep, toks)
}
// store new commandOnStep
dbg.commandOnStepStored = dbg.commandOnStep
// display the new ONSTEP command(s)
s := strings.Builder{}
for _, c := range dbg.commandOnStep {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on step: %s", strings.TrimSuffix(s.String(), "; "))
return nil
case cmdOnTrace:
if tokens.Remaining() == 0 {
if len(dbg.commandOnTrace) == 0 {
dbg.printLine(terminal.StyleFeedback, "no command on trace")
} else {
s := strings.Builder{}
for _, c := range dbg.commandOnTrace {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on trace: %s", strings.TrimSuffix(s.String(), "; "))
}
return nil
}
var input string
option, _ := tokens.Get()
switch strings.ToUpper(option) {
case "OFF":
dbg.commandOnTrace = dbg.commandOnTrace[:0]
dbg.printLine(terminal.StyleFeedback, "auto-command on trace: OFF")
return nil
case "ON":
dbg.commandOnTrace = dbg.commandOnTraceStored
for _, c := range dbg.commandOnTrace {
dbg.printLine(terminal.StyleFeedback, "auto-command on trace: %s", c)
}
return nil
default:
// token isn't one we recognise so push it back onto the token queue
tokens.Unget()
// use remaininder of command line to form the ONTRACE command sequence
input = strings.TrimSpace(tokens.Remainder())
tokens.End()
}
// empty list of tokens. taking note of existing command
existingOnTrace := dbg.commandOnTrace
dbg.commandOnTrace = dbg.commandOnTrace[:0]
// tokenise commands to check for integrity
for _, s := range strings.Split(input, ",") {
toks, err := dbg.tokeniseCommand(s, false, false)
if err != nil {
dbg.commandOnTrace = existingOnTrace
return err
}
dbg.commandOnTrace = append(dbg.commandOnTrace, toks)
}
// store new commandOnTrace
dbg.commandOnTraceStored = dbg.commandOnTrace
// display the new ONTRACE command(s)
s := strings.Builder{}
for _, c := range dbg.commandOnTrace {
s.WriteString(c.String())
s.WriteString("; ")
}
dbg.printLine(terminal.StyleFeedback, "command on trace: %s", strings.TrimSuffix(s.String(), "; "))
return nil
case cmdLast:
// if debugger is running in a non-instruction quantum then the live disasm
// information will not have been updated. for the purposes of the last
// instruction however, we definitely do want that information to be
// current
if dbg.running && dbg.quantum.Load() != govern.QuantumInstruction {
dbg.liveBankInfo = dbg.vcs.Mem.Cart.GetBank(dbg.vcs.CPU.PC.Address())
dbg.liveDisasmEntry = dbg.Disasm.ExecutedEntry(dbg.liveBankInfo, dbg.vcs.CPU.LastResult, true, dbg.vcs.CPU.PC.Value())
}
if dbg.liveDisasmEntry == nil || dbg.liveDisasmEntry.Result.Defn == nil {
dbg.printLine(terminal.StyleFeedback, "no instruction decoded yet")
return nil
}
// whether to show bytecode
bytecode := false
option, ok := tokens.Get()
if ok {
switch strings.ToUpper(option) {
case "DEFN":
if dbg.vcs.CPU.LastResult.Defn == nil {
dbg.printLine(terminal.StyleFeedback, "no instruction decoded yet")
} else {
dbg.printLine(terminal.StyleFeedback, "%s", dbg.vcs.CPU.LastResult.Defn)
}
return nil
case "BYTECODE":
bytecode = true
}
}
s := strings.Builder{}
if dbg.vcs.Mem.Cart.NumBanks() > 1 {
s.WriteString(fmt.Sprintf("[%s] ", dbg.liveBankInfo))
}
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldAddress))
s.WriteString(" ")
if bytecode {
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldBytecode))
s.WriteString(" ")
}
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldOperator))
s.WriteString(" ")
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldOperand))
s.WriteString(" ")
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldCycles))
s.WriteString(" ")
s.WriteString(dbg.liveDisasmEntry.GetField(disassembly.FldNotes))
// change terminal output style depending on condition of last CPU result
if dbg.liveDisasmEntry.Result.Final {
dbg.printLine(terminal.StyleInstructionStep, s.String())
} else {
dbg.printLine(terminal.StyleSubStep, s.String())
}
case cmdMemMap:
address, ok := tokens.Get()
if ok {
// if an address argument has been specified then map the address
// in a read and write context and display the information
// if hasMapped is false after the read/write mappings then the
// address could no be resolved and we print an appropriate notice
// to the user
hasMapped := false
s := strings.Builder{}
ai := dbg.dbgmem.GetAddressInfo(address, true)
if ai != nil {
hasMapped = true
s.WriteString("Read:\n")
if ai.Address != ai.MappedAddress {
s.WriteString(fmt.Sprintf(" %#04x maps to %#04x ", ai.Address, ai.MappedAddress))
} else {
s.WriteString(fmt.Sprintf(" %#04x ", ai.Address))
}
s.WriteString(fmt.Sprintf("in area %s\n", ai.Area.String()))
if ai.Symbol != "" {
s.WriteString(fmt.Sprintf(" labelled as %s\n", ai.Symbol))
}
}
ai = dbg.dbgmem.GetAddressInfo(address, false)
if ai != nil {
hasMapped = true
s.WriteString("Write:\n")
if ai.Address != ai.MappedAddress {
s.WriteString(fmt.Sprintf(" %#04x maps to %#04x ", ai.Address, ai.MappedAddress))
} else {
s.WriteString(fmt.Sprintf(" %#04x ", ai.Address))
}
s.WriteString(fmt.Sprintf("in area %s\n", ai.Area.String()))
if ai.Symbol != "" {
s.WriteString(fmt.Sprintf(" labelled as %s\n", ai.Symbol))
}
}
// print results
if hasMapped {
dbg.printLine(terminal.StyleInstrument, "%s", s.String())
} else {
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("%v is not a mappable address", address))
}
} else {
// without an address argument print the memorymap summary table
dbg.printLine(terminal.StyleInstrument, "%v", memorymap.Summary())
}
case cmdCPU:
action, ok := tokens.Get()
if ok {
switch strings.ToUpper(action) {
case "STATUS":
action, ok = tokens.Get()
if ok {
target, _ := tokens.Get()
var targetVal *bool
switch target {
case "S":
targetVal = &dbg.vcs.CPU.Status.Sign
case "O":
targetVal = &dbg.vcs.CPU.Status.Overflow
case "B":
targetVal = &dbg.vcs.CPU.Status.Break
case "D":
targetVal = &dbg.vcs.CPU.Status.DecimalMode
case "I":
targetVal = &dbg.vcs.CPU.Status.InterruptDisable
case "Z":
targetVal = &dbg.vcs.CPU.Status.Zero
case "C":
targetVal = &dbg.vcs.CPU.Status.Carry
}
switch action {
case "SET":
*targetVal = true
case "UNSET":
*targetVal = false
case "TOGGLE":
*targetVal = !*targetVal
}
} else {
dbg.printLine(terminal.StyleInstrument, dbg.vcs.CPU.Status.String())
}
case "SET":
target, _ := tokens.Get()
value, _ := tokens.Get()
target = strings.ToUpper(target)
if target == "PC" {
// program counter is a 16 bit number
v, err := strconv.ParseUint(value, 0, 16)
if err != nil {
dbg.printLine(terminal.StyleError, "value must be a positive 16 bit number")
}
dbg.vcs.CPU.PC.Load(uint16(v))
} else {
// 6507 registers are 8 bit
v, err := strconv.ParseUint(value, 0, 8)
if err != nil {
dbg.printLine(terminal.StyleError, "value must be a positive 8 bit number")
}
var reg *registers.Register
switch strings.ToUpper(target) {
case "A":
reg = &dbg.vcs.CPU.A
case "X":
reg = &dbg.vcs.CPU.X
case "Y":
reg = &dbg.vcs.CPU.Y
case "SP":
reg = &dbg.vcs.CPU.SP.Register
}
reg.Load(uint8(v))
}
default:
// already caught by command line ValidateTokens()
}
} else {
dbg.printLine(terminal.StyleInstrument, dbg.vcs.CPU.String())
}
case cmdBus:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.Mem.String())
action, ok := tokens.Get()
if ok {
switch strings.ToUpper(action) {
case "DETAIL":
_, area := memorymap.MapAddress(dbg.vcs.Mem.AddressBus, !dbg.vcs.Mem.LastCPUWrite)
access := "reading"
if dbg.vcs.Mem.LastCPUWrite {
access = "writing"
}
dbg.printLine(terminal.StyleInstrument, fmt.Sprintf("%s (%s)", area.String(), access))
default:
// already caught by command line ValidateTokens()
}
}
case cmdPeek:
// get first address token
a, ok := tokens.Get()
for ok {
// perform peek
ai, err := dbg.dbgmem.Peek(a)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
} else {
dbg.printLine(terminal.StyleInstrument, ai.String())
}
// loop through all addresses
a, ok = tokens.Get()
}
case cmdPoke:
// get address token
a, _ := tokens.Get()
// convert address. note that the calls to dbgmem.poke() also call
// MapAddress(). the reason we map the address here is because we want
// a numeric address that we can iterate with in the for loop below.
// simply converting to a number is no good because we want the user to
// be able to specify an address by name, so we may as well just call
// MapAddress(), even if it does seem redundant
//
// see comment in DbgMem.Poke() for why we treat the address as a
// "read" address
ai := dbg.dbgmem.GetAddressInfo(a, true)
if ai == nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s: %v", dbgmem.PokeError, a))
return nil
}
addr := ai.MappedAddress
// get (first) value token
v, ok := tokens.Get()
for ok {
val, err := strconv.ParseUint(v, 0, 8)
if err != nil {
dbg.printLine(terminal.StyleError, "value must be an 8 bit number (%s)", v)
v, ok = tokens.Get()
continue // for loop (without advancing address)
}
ai, err := dbg.dbgmem.Poke(addr, uint8(val))
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
} else {
dbg.printLine(terminal.StyleInstrument, ai.String())
}
// loop through all values
v, ok = tokens.Get()
addr++
}
case cmdSwap:
// get address token
a, _ := tokens.Get()
b, _ := tokens.Get()
ai, err := dbg.dbgmem.Peek(a)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
bi, err := dbg.dbgmem.Peek(b)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
if _, err := dbg.dbgmem.Poke(ai.MappedAddress, bi.Data); err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
if _, err := dbg.dbgmem.Poke(bi.MappedAddress, ai.Data); err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
aj, err := dbg.dbgmem.Peek(ai.Address)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
bj, err := dbg.dbgmem.Peek(bi.Address)
if err != nil {
dbg.printLine(terminal.StyleError, "%s", err)
return nil
}
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("%04x: %02x->%02x and %04x: %02x->%02x", ai.Address, ai.Data, aj.Data, bi.Address, bi.Data, bj.Data))
case cmdRAM:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.Mem.RAM.String())
case cmdTIA:
arg, _ := tokens.Get()
switch arg {
case "HMOVE":
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Hmove.String())
default:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.String())
}
case cmdRIOT:
arg, _ := tokens.Get()
switch arg {
case "TIMER":
dbg.printLine(terminal.StyleInstrument, dbg.vcs.RIOT.Timer.String())
case "PORTS":
fallthrough
default:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.RIOT.Ports.String())
}
case cmdAudio:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Audio.String())
case cmdTV:
option, ok := tokens.Get()
if ok {
option = strings.ToUpper(option)
switch option {
case "SPEC":
newspec, ok := tokens.Get()
if ok {
// unknown specifciations already handled by ValidateTokens()
err := dbg.vcs.TV.SetSpec(newspec)
if err != nil {
return err
}
}
spec := dbg.vcs.TV.GetFrameInfo().Spec
s := strings.Builder{}
s.WriteString(spec.ID)
dbg.printLine(terminal.StyleInstrument, s.String())
default:
// already caught by command line ValidateTokens()
}
} else {
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TV.String())
}
// information about the machine (sprites, playfield)
case cmdPlayer:
plyr := -1
arg, _ := tokens.Get()
switch arg {
case "0":
plyr = 0
case "1":
plyr = 1
}
switch plyr {
case 0:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Player0.String())
case 1:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Player1.String())
default:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Player0.String())
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Player1.String())
}
case cmdMissile:
miss := -1
arg, _ := tokens.Get()
switch arg {
case "0":
miss = 0
case "1":
miss = 1
}
switch miss {
case 0:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Missile0.String())
case 1:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Missile1.String())
default:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Missile0.String())
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Missile1.String())
}
case cmdBall:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Ball.String())
case cmdPlayfield:
dbg.printLine(terminal.StyleInstrument, dbg.vcs.TIA.Video.Playfield.String())
case cmdPlusROM:
plusrom, ok := dbg.vcs.Mem.Cart.GetContainer().(*plusrom.PlusROM)
if !ok {
dbg.printLine(terminal.StyleError, "not a plusrom cartridge")
return nil
}
option, _ := tokens.Get()
switch option {
case "NICK":
nick, _ := tokens.Get()
err := dbg.vcs.Env.Prefs.PlusROM.Nick.Set(nick)
if err != nil {
return err
}
err = dbg.vcs.Env.Prefs.PlusROM.Save()
if err != nil {
return err
}
case "ID":
id, _ := tokens.Get()
err := dbg.vcs.Env.Prefs.PlusROM.ID.Set(id)
if err != nil {
return err
}
err = dbg.vcs.Env.Prefs.PlusROM.Save()
if err != nil {
return err
}
case "HOST":
ai := plusrom.GetAddrInfo()
host, _ := tokens.Get()
plusrom.SetAddrInfo(host, ai.Path)
case "PATH":
ai := plusrom.GetAddrInfo()
path, _ := tokens.Get()
plusrom.SetAddrInfo(ai.Host, path)
default:
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("Nick: %s", dbg.vcs.Env.Prefs.PlusROM.Nick.String()))
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("ID: %s", dbg.vcs.Env.Prefs.PlusROM.ID.String()))
ai := plusrom.GetAddrInfo()
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("Host: %s", ai.Host))
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("Path: %s", ai.Path))
}
case cmdCoProc:
bus := dbg.vcs.Mem.Cart.GetCoProcBus()
if bus == nil {
dbg.printLine(terminal.StyleError, "cartridge does not have a coprocessor")
return nil
}
option, _ := tokens.Get()
switch option {
case "TOP":
top := 10 // default of top 10
arg, ok := tokens.Get()
if ok {
n, err := strconv.ParseInt(arg, 0, 32)
if err != nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s is not a number", arg))
return nil
}
top = int(n)
}
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source files found")
return
}
for i := 0; i < top; i++ {
f := src.SortedFunctions.Functions[i]
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("%02d: %s", i, f.Name))
}
})
case "LIST":
arg, _ := tokens.Get()
switch arg {
case "FAULTS":
dbg.CoProcDev.BorrowFaults(func(flt *faults.Faults) {
w := dbg.writerInStyle(terminal.StyleFeedback)
flt.WriteLog(w)
})
case "SOURCEFILES":
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source files found")
return
}
for _, fn := range src.Files {
dbg.printLine(terminal.StyleFeedback, fn.Filename)
}
})
case "FUNCTIONS":
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source files found")
return
}
for _, n := range src.FunctionNames {
fn := src.Functions[n]
dbg.printLine(terminal.StyleFeedback, fn.String())
}
})
default:
}
case "MEM":
bus := dbg.vcs.Mem.Cart.GetStaticBus()
if bus == nil {
dbg.printLine(terminal.StyleError, "cartridge does not have any coprocessor memory")
return nil
}
static := bus.GetStatic()
if static == nil {
dbg.printLine(terminal.StyleError, "cartridge does not have any coprocessor memory")
return nil
}
arg, _ := tokens.Get()
switch arg {
case "DUMP":
dump := func(name string) {
if data, ok := static.Reference(name); ok {
fn := unique.Filename(fmt.Sprintf("dump_%s", name), dbg.loader.ShortName())
fn = fmt.Sprintf("%s.bin", fn)
err := os.WriteFile(fn, data, 0644)
if err != nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("error writing %s", fn))
} else {
dbg.printLine(terminal.StyleFeedback, fn)
}
}
}
if arg, ok := tokens.Get(); ok {
dump(arg)
} else {
for _, seg := range static.Segments() {
dump(seg.Name)
}
}
default:
for _, seg := range static.Segments() {
s := fmt.Sprintf("%s: %08x to %08x", seg.Name, seg.Origin, seg.Memtop)
dbg.printLine(terminal.StyleFeedback, s)
}
}
case "REGS":
coproc := bus.GetCoProc()
// list registers in order until we get a not-ok reply
regs := func(group coprocessor.ExtendedRegisterGroup) {
s := strings.Builder{}
for r := group.Start; r <= group.End; r++ {
v, f, ok := coproc.RegisterFormatted(r)
if !ok {
dbg.printLine(terminal.StyleError,
fmt.Sprintf("coprocessor doesn't have the %d register in the %s group", r, group.Name))
return
}
if group.Formatted {
s.WriteString(fmt.Sprintf("%s: %s [%08x]\t", group.Label(r), f, v))
} else {
s.WriteString(fmt.Sprintf("%s: %08x\t", group.Label(r), v))
}
if (r-group.Start+1)%3 == 0 {
dbg.printLine(terminal.StyleFeedback, s.String())
s.Reset()
}
}
if s.Len() > 0 {
dbg.printLine(terminal.StyleFeedback, s.String())
}
}
// use named group if supplied or default to core group
spec := coproc.RegisterSpec()
if arg, ok := tokens.Get(); ok {
if group, ok := spec.Group(arg); ok {
regs(group)
} else {
dbg.printLine(terminal.StyleError, fmt.Sprintf("coprocessor doesn't have a %s register group", arg))
}
} else {
if group, ok := spec.Group(coprocessor.ExtendedRegisterCoreGroup); ok {
regs(group)
} else {
dbg.printLine(terminal.StyleError, "coprocessor doesn't seem to have any registers")
}
}
case "SET":
var reg int
var value uint32
arg, ok := tokens.Get()
if ok {
n, err := strconv.ParseInt(arg, 0, 32)
if err != nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s is not a number", arg))
return nil
}
reg = int(n)
}
arg, ok = tokens.Get()
if ok {
n, err := strconv.ParseInt(arg, 0, 32)
if err != nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s is not a number", arg))
return nil
}
value = uint32(n)
}
if bus.GetCoProc().RegisterSet(reg, value) {
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("setting coproc register %d to %08x\n", reg, value))
} else {
dbg.printLine(terminal.StyleError, fmt.Sprintf("cannot set coproc register %d to %08x\n", reg, value))
}
case "STEP":
dbg.CoProcDev.BreakNextInstruction()
dbg.runUntilHalt = true
dbg.continueEmulation = true
case "ID":
fallthrough
default:
dbg.printLine(terminal.StyleFeedback, bus.GetCoProc().ProcessorID())
}
case cmdDWARF:
coproc := dbg.vcs.Mem.Cart.GetCoProcBus()
if coproc == nil {
dbg.printLine(terminal.StyleError, "cartridge does not have a coprocessor")
return nil
}
option, _ := tokens.Get()
switch option {
case "FUNCTIONS":
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source available")
return
}
for _, n := range src.FunctionNames {
f := src.Functions[n]
dbg.printLine(terminal.StyleFeedback, f.String())
}
})
case "GLOBALS":
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source available")
return
}
for _, g := range src.SortedGlobals.Variables {
g.Update()
dbg.printLine(terminal.StyleFeedback, g.String())
}
})
case "LOCALS":
var derivation bool
var ranges bool
var err bool
option, ok := tokens.Get()
for ok {
derivation = derivation || option == "DERIVATION"
ranges = ranges || option == "RANGES"
err = err || option == "ERROR"
option, ok = tokens.Get()
}
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source available")
}
})
dbg.CoProcDev.BorrowYieldState(func(yld yield.State) {
var w io.Writer
if derivation {
w = dbg.writerInStyle(terminal.StyleFeedbackSecondary, "\t")
}
for _, l := range yld.LocalVariables {
dbg.printLine(terminal.StyleFeedback, l.String())
e := l.WriteDerivation(w)
if err && e != nil {
for _, s := range strings.Split(e.Error(), ":") {
dbg.printLine(terminal.StyleError, fmt.Sprintf("\t%s", s))
}
}
if ranges {
dbg.printLine(terminal.StyleFeedbackSecondary, fmt.Sprintf("\t%s", l.Range.String()))
}
}
})
case "FRAMEBASE":
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source available")
}
var derivation bool
option, ok := tokens.Get()
for ok {
derivation = derivation || option == "DERIVATION"
option, ok = tokens.Get()
}
var w io.Writer
if derivation {
w = dbg.writerInStyle(terminal.StyleFeedback)
}
fb, err := src.FramebaseCurrent(w)
if err != nil {
dbg.printLine(terminal.StyleError, err.Error())
} else {
dbg.printLine(terminal.StyleFeedback, fmt.Sprintf("%08x", fb))
}
})
case "LINE":
arg, ok := tokens.Get()
if !ok {
dbg.printLine(terminal.StyleError, "command requires argument file:line")
return nil
}
// option is divided by a maximum of one colon, meaning the split
// array should be a length of two
s := strings.Split(arg, ":")
if len(s) != 2 {
dbg.printLine(terminal.StyleError, "command requires argument file:line")
return nil
}
// filename and line number
fn := s[0]
n, err := strconv.ParseInt(s[1], 0, 32)
if err != nil {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s is not a number", s[1]))
return nil
}
ln := int(n)
dbg.CoProcDev.BorrowSource(func(src *dwarf.Source) {
if src == nil {
dbg.printLine(terminal.StyleError, "no source available")
}
// check file by shortname and then by full name
f, ok := src.FilesByShortname[fn]
if !ok {
f, ok = src.Files[fn]
if !ok {
dbg.printLine(terminal.StyleError, fmt.Sprintf("no file named %s", fn))
return
}
}
// line numbers are counted from one
if ln < 1 {
ln = 1
}
if ln > len(f.Content.Lines) {
dbg.printLine(terminal.StyleError, fmt.Sprintf("%s only has %d lines", fn, len(f.Content.Lines)))
return
}
l := f.Content.Lines[ln-1]
// display what we know about line
dbg.printLine(terminal.StyleFeedback, l.String())
})
case "CALLSTACK":
dbg.CoProcDev.BorrowCallStack(func(callstack callstack.CallStack) {
w := dbg.writerInStyle(terminal.StyleFeedback)
callstack.WriteCallStack(w)
})
case "CALLERS":
arg, ok := tokens.Get()
if !ok {
dbg.printLine(terminal.StyleError, "function name is required")
return nil
}
dbg.CoProcDev.BorrowCallStack(func(callstack callstack.CallStack) {
w := dbg.writerInStyle(terminal.StyleFeedback)
if err := callstack.WriteCallers(arg, w); err != nil {
dbg.printLine(terminal.StyleError, err.Error())
return
}
})
}
case cmdPeripheral:
player, _ := tokens.Get()
var id plugging.PortID
switch strings.ToUpper(player) {
case "LEFT":
id = plugging.PortLeft
case "RIGHT":
id = plugging.PortRight
case "SWAP":
if dbg.controllers.Swap() {
dbg.printLine(terminal.StyleFeedback, "player peripherals are in the swapped state")
} else {
dbg.printLine(terminal.StyleFeedback, "player peripherals are in the normal state")
}
return nil
}
var err error
controller, ok := tokens.Get()
if ok {
switch strings.ToUpper(controller) {
case "AUTO":
dbg.vcs.FingerprintPeripheral(id, *dbg.loader)
case "STICK":
err = dbg.vcs.RIOT.Ports.Plug(id, controllers.NewStick)
case "PADDLE":
err = dbg.vcs.RIOT.Ports.Plug(id, controllers.NewPaddlePair)
case "KEYPAD":
err = dbg.vcs.RIOT.Ports.Plug(id, controllers.NewKeypad)
case "GAMEPAD":
err = dbg.vcs.RIOT.Ports.Plug(id, controllers.NewGamepad)
case "SAVEKEY":
err = dbg.vcs.RIOT.Ports.Plug(id, savekey.NewSaveKey)
case "ATARIVOX":
err = dbg.vcs.RIOT.Ports.Plug(id, atarivox.NewAtariVox)
}
}
if err != nil {
return err
}
var p ports.Peripheral
switch strings.ToUpper(player) {
case "LEFT":
p = dbg.vcs.RIOT.Ports.LeftPlayer
case "RIGHT":
p = dbg.vcs.RIOT.Ports.RightPlayer
}
dbg.printLine(terminal.StyleInstrument, p.String())
case cmdPanel:
mode, ok := tokens.Get()
if !ok {
dbg.printLine(terminal.StyleInstrument, dbg.vcs.RIOT.Ports.Panel.String())
return nil
}
var err error
switch strings.ToUpper(mode) {
case "TOGGLE":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "P0":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer0Pro, D: nil}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelTogglePlayer1Pro, D: nil}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelToggleColor, D: nil}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "SET":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "P0PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: true}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1PRO":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: true}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P0AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer0Pro, D: false}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "P1AM":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetPlayer1Pro, D: false}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "COL":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: true}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "BW":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSetColor, D: false}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "HOLD":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: true}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: true}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "RELEASE":
arg, _ := tokens.Get()
switch strings.ToUpper(arg) {
case "SELECT":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelSelect, D: false}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RESET":
inp := ports.InputEvent{Port: plugging.PortPanel, Ev: ports.PanelReset, D: false}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
}
if err != nil {
return err
}
dbg.printLine(terminal.StyleInstrument, dbg.vcs.RIOT.Ports.Panel.String())
case cmdStick:
var err error
port, _ := tokens.Get()
action, _ := tokens.Get()
var event ports.Event
var value ports.EventData
switch strings.ToUpper(action) {
case "FIRE":
event = ports.Fire
value = ports.DataStickTrue
case "UP":
event = ports.Up
value = ports.DataStickTrue
case "DOWN":
event = ports.Down
value = ports.DataStickTrue
case "LEFT":
event = ports.Left
value = ports.DataStickTrue
case "RIGHT":
event = ports.Right
value = ports.DataStickTrue
case "NOFIRE":
event = ports.Fire
value = ports.DataStickFalse
case "NOUP":
event = ports.Up
value = ports.DataStickFalse
case "NODOWN":
event = ports.Down
value = ports.DataStickFalse
case "NOLEFT":
event = ports.Left
value = ports.DataStickFalse
case "NORIGHT":
event = ports.Right
value = ports.DataStickFalse
}
switch port {
case "LEFT":
inp := ports.InputEvent{Port: plugging.PortLeft, Ev: event, D: value}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
case "RIGHT":
inp := ports.InputEvent{Port: plugging.PortRight, Ev: event, D: value}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
if err != nil {
return err
}
case cmdKeypad:
var err error
port, _ := tokens.Get()
key, _ := tokens.Get()
switch port {
case "LEFT":
if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortLeft, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
} else {
inp := ports.InputEvent{Port: plugging.PortLeft, Ev: ports.KeypadDown, D: rune(key[0])}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
case "RIGHT":
if strings.ToUpper(key) == "NONE" {
inp := ports.InputEvent{Port: plugging.PortRight, Ev: ports.KeypadUp, D: nil}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
} else {
inp := ports.InputEvent{Port: plugging.PortLeft, Ev: ports.KeypadDown, D: rune(key[0])}
_, err = dbg.vcs.Input.HandleInputEvent(inp)
}
}
if err != nil {
return err
}
case cmdBreak:
err := dbg.halting.breakpoints.parseCommand(tokens)
if err != nil {
return err
}
case cmdTrap:
err := dbg.halting.traps.parseCommand(tokens)
if err != nil {
return err
}
case cmdWatch:
err := dbg.halting.watches.parseCommand(tokens)
if err != nil {
return err
}
case cmdTrace:
err := dbg.traces.parseCommand(tokens)
if err != nil {
return err
}
case cmdList:
list, _ := tokens.Get()
list = strings.ToUpper(list)
switch list {
case "BREAKS":
dbg.halting.breakpoints.list()
dbg.CoProcDev.BorrowBreakpoints(func(bp coproc_breakpoints.Breakpoints) {
w := dbg.writerInStyle(terminal.StyleFeedback)
bp.Write(w)
})
case "TRAPS":
dbg.halting.traps.list()
case "WATCHES":
dbg.halting.watches.list()
case "TRACES":
dbg.traces.list()
case "ALL":
dbg.halting.breakpoints.list()
dbg.halting.traps.list()
dbg.halting.watches.list()
dbg.traces.list()
default:
// already caught by command line ValidateTokens()
}
case cmdDrop:
drop, _ := tokens.Get()
s, _ := tokens.Get()
num, err := strconv.Atoi(s)
if err != nil {
return fmt.Errorf("drop attribute must be a number (%s)", s)
}
drop = strings.ToUpper(drop)
switch drop {
case "BREAK":
err := dbg.halting.breakpoints.drop(num)
if err != nil {
return err
}
dbg.printLine(terminal.StyleFeedback, "breakpoint #%d dropped", num)
case "TRAP":
err := dbg.halting.traps.drop(num)
if err != nil {
return err
}
dbg.printLine(terminal.StyleFeedback, "trap #%d dropped", num)
case "WATCH":
err := dbg.halting.watches.drop(num)
if err != nil {
return err
}
dbg.printLine(terminal.StyleFeedback, "watch #%d dropped", num)
case "TRACE":
err := dbg.traces.drop(num)
if err != nil {
return err
}
dbg.printLine(terminal.StyleFeedback, "trace #%d dropped", num)
default:
// already caught by command line ValidateTokens()
}
case cmdClear:
clear, _ := tokens.Get()
clear = strings.ToUpper(clear)
switch clear {
case "BREAKS":
dbg.halting.breakpoints.clear()
dbg.printLine(terminal.StyleFeedback, "breakpoints cleared")
case "TRAPS":
dbg.halting.traps.clear()
dbg.printLine(terminal.StyleFeedback, "traps cleared")
case "WATCHES":
dbg.halting.watches.clear()
dbg.printLine(terminal.StyleFeedback, "watches cleared")
case "TRACES":
dbg.traces.clear()
dbg.printLine(terminal.StyleFeedback, "traces cleared")
case "ALL":
dbg.halting.breakpoints.clear()
dbg.halting.traps.clear()
dbg.halting.watches.clear()
dbg.traces.clear()
dbg.printLine(terminal.StyleFeedback, "breakpoints, traps, watches and traces cleared")
default:
// already caught by command line ValidateTokens()
}
case cmdLog:
option, ok := tokens.Get()
if ok {
option = strings.ToUpper(option)
switch option {
case "LAST":
s := &strings.Builder{}
logger.Tail(s, 1)
dbg.printLine(terminal.StyleLog, s.String())
case "RECENT":
s := &strings.Builder{}
logger.WriteRecent(s)
dbg.printLine(terminal.StyleLog, s.String())
case "CLEAR":
logger.Clear()
}
} else {
s := &strings.Builder{}
logger.Write(s)
if s.Len() == 0 {
dbg.printLine(terminal.StyleFeedback, "log is empty")
} else {
dbg.printLine(terminal.StyleLog, s.String())
}
}
case cmdMemUsage:
var m runtime.MemStats
runtime.ReadMemStats(&m)
s := strings.Builder{}
s.WriteString(fmt.Sprintf("Alloc = %v MB\n", m.Alloc/1048576))
s.WriteString(fmt.Sprintf(" TotalAlloc = %v MB\n", m.TotalAlloc/1048576))
s.WriteString(fmt.Sprintf(" Sys = %v MB\n", m.Sys/1048576))
s.WriteString(fmt.Sprintf(" NumGC = %v", m.NumGC))
dbg.printLine(terminal.StyleLog, s.String())
case cmdVersion:
dbg.printLine(terminal.StyleLog, version.Version)
option, ok := tokens.Get()
if ok {
option = strings.ToUpper(option)
switch option {
case "REVISION":
dbg.printLine(terminal.StyleLog, version.Revision)
}
}
}
return nil
}