1// 2// Copyright (c) 2024 Freddy Vandriessche. 3// notice: https://raw.githubusercontent.com/RetroAppleJS/AppleII-IDE/main/LICENSE.md 4// 5// apple2debug.js 6// 7// STEP TRACE is an attached debugger for the *live* Apple II runtime. 8// It never owns a shadow CPU or RAM image. Execution is performed by the real 9// Apple2Plus CPU/hardware/peripheral objects and disassembly reads the currently 10// mapped CPU bus through safe_read(). 11// 12 13if(oEMU===undefined) var oEMU = {"component":{"CPU":{"Apple2Debug":new Apple2Debug()}}} 14else oEMU.component.CPU.Apple2Debug = new Apple2Debug(); 15 16function Apple2Debug() 17{ 18 var dbg = this; 19 var cpu_config = null; 20 var max_instrlen = 3; 21 var oDASM_debug = null; 22 var oListingASM = null; 23 24 var listingRows = 20; 25 var currentPC = null; 26 var previousObservedPC = null; 27 var previousBoundary = new Int32Array(0x10000); 28 previousBoundary.fill(-1); 29 30 // Cached decoding is always validated against the bytes currently visible 31 // on the mapped CPU bus. Self-modifying code/remapping therefore invalidates 32 // only the local decode and any predecessor edge derived from the old bytes. 33 var decodeCache = new Array(0x10000); 34 var cacheHits = 0; 35 var cacheMisses = 0; 36 var domWrites = 0; 37 var lastRegisterHTML = null; 38 39 var listingColumns = "{adr:0,code:6,lin:15,lbl:21,ins:30,opr:35,com:51}"; 40 var listingColumnPresets = { 41 "default":"{adr:0,code:6,lin:15,lbl:21,ins:30,opr:35,com:51}" 42 ,"wide":"{adr:0,code:6,lin:17,lbl:24,ins:34,opr:40,com:60}" 43 ,"compact":"{adr:0,code:6,lbl:15,ins:24,opr:29,com:45}" 44 }; 45 46 // A symbol export from the assembler can be attached to the live trace. 47 // Labels populate lbl, label/EQU values can replace numeric operands, and 48 // instruction comments populate com. The maps are address keyed to keep 49 // lookup cost constant while the live CPU is running. 50 var loadedLabels = new Array(0x10000);
51 var loadedSymbols = new Array(0x10000); 52 var loadedComments = new Array(0x10000); 53 var loadedSymbolNames = Object.create(null); 54 var symbolState = { 55 file:"" 56 ,source:"" 57 ,format:"" 58 ,labels:0 59 ,equs:0 60 ,comments:0 61 ,error:"" 62 }; 63 64 // Same Unicode-capable mono stack as the assembler Source/Listing panes. 65 var listingFontFamily = '"DejaVu Sans Mono","Menlo","Consolas","Courier New",monospace'; 66 var listingFontSize = 9; 67 var rowPixelHeight = 12; 68 69 // Listing viewport state. Manual navigation walks decoded instruction rows, 70 // never address deltas. 71 var followPC = true; 72 // Closed-loop step skipper. `showLoopSteps` is retained as the compatibility 73 // state bit: false means the skipper is enabled. The first taken backward 74 // branch/JMP proves a loop; subsequent instructions in that proven loop are 75 // executed in zero-delay cooperative bursts instead of waiting for the fixed 76 // IPS cadence. The complete debugger display refreshes at the exact loop exit. 77 var showLoopSteps = true; 78 var closedLoopSkipBatch = 1024; 79 var activeClosedLoop = null; 80 var loopDisplayStats = {detected:0,hiddenInstructions:0,exits:0}; 81 var manualTop = null; 82 var lastViewTop = null; 83 var lastRows = []; 84 85 // Run control. "system" delegates execution to the central emulator speed; 86 // fixed IPS modes own instruction scheduling while the SYSTEM timer is paused. 87 var runMode = "system"; 88 var fixedRunning = false; 89 var runTimer = null; 90 var fixedNextDueMs = null; 91 var resumePct = 1; 92 93 // Step Over/Out are temporary debugger-owned live boundary runs. 94 var boundaryAction = null; 95 96 // Manual Step In/Over/Out leaves the main execution indicator paused. 97 var manualStepPause = false; 98 99 // Conditional breakpoint. While armed, its predicate is evaluated at every 100 // clean live instruction boundary before interrupt dispatch/opcode fetch. A 101 // false result keeps it armed; a true result stops before the opcode fetch. 102 var breakConditionText = ""; 103 var breakConditionError = ""; 104 var breakConditionAst = null; 105 var conditionHelp = "Conditional breakpoint. Registers/counters: A X Y SP P PC INS. INS is the 48-bit completed-instruction count shown in NAV. Flags: N V B D I Z C. Memory: M[$addr] or MEM[$addr], M16[$addr] or MEM16[$addr]. Operators: == != < <= > >= & | ^ && || ! and parentheses."; 106 var conditionalBreakpoint = { 107 armed:false 108 ,hit:false 109 ,hits:0 110 ,checks:0 111 ,skips:0 112 ,condition:"" 113 ,mode:"" 114 ,address:null 115 ,lastResult:null 116 ,error:null 117 }; 118 var breakMessage = ""; 119 120 // Optional synchronous action dispatched when BREAK IF matches. The live 121 // emulator remains the execution owner; a normal callback return means the 122 // matching opcode may execute immediately. 123 var breakpointActionHandler = null; 124 var breakpointActionDispatching = false; 125 var breakpointActionLastError = null; 126 127 function liveMachine() 128 { 129 return typeof(apple2plus)=="object" && apple2plus ? apple2plus : null; 130 } 131 132 function liveCPU() 133 { 134 var machine = liveMachine(); 135 return machine && typeof(machine.cpuObj)=="function" ? machine.cpuObj() : null; 136 } 137 138 function liveHW() 139 { 140 var machine = liveMachine(); 141 return machine && typeof(machine.hwObj)=="function" ? machine.hwObj() : null; 142 } 143 144 /* 145 * STEP TRACE has one deliberately masked CPU-bus interval: $C000-$C0FF. 146 * That page contains motherboard and slot soft-switch I/O rather than code. 147 * Everything else, including the complete peripheral/expansion ROM window 148 * $C100-$CFFF, is read through Apple2Hw.safe_read() exactly as the CPU maps it. 149 */ 150 function traceAddressReadable(addr) 151 { 152 addr &= 0xffff; 153 return addr<0xc000 || addr>=0xc100; 154 } 155 156 function traceRead(addr) 157 { 158 addr &= 0xffff; 159 if(!traceAddressReadable(addr)) return null; 160 var hw = liveHW(); 161 if(!hw || typeof(hw.safe_read)!="function") return null; 162 return hw.safe_read(addr) & 0xff;
163 } 164 165 function peripheralRomAddress(addr) 166 { 167 addr &= 0xffff; 168 return addr>=0xc100 && addr<=0xcfff; 169 } 170 171 function currentAsmSymlink() 172 { 173 if(typeof(oASM)!="undefined" && oASM && oASM.symlink) return oASM.symlink; 174 if(typeof(asm)!="undefined" && asm && asm.symlink) return asm.symlink; 175 return null; 176 } 177 178 function symbolAtValue(value) 179 { 180 value &= 0xffff; 181 if(loadedSymbols[value]) return loadedSymbols[value]; 182 var symlink = currentAsmSymlink(); 183 return symlink && typeof(symlink[value])!=="undefined" ? symlink[value] : null; 184 } 185 186 function labelAtAddress(addr) 187 { 188 return loadedLabels[addr & 0xffff] || ""; 189 } 190 191 function commentAtAddress(addr,d) 192 { 193 var records = loadedComments[addr & 0xffff]; 194 if(!records || !records.length) return ""; 195 var bytes = [d.b0,d.b1,d.b2]; 196 var out = []; 197 198 for(var i=0;i<records.length;i++) 199 { 200 var record = records[i]; 201 var opcode = record.opcode || []; 202 var match = true; 203 for(var j=0;j<opcode.length;j++) 204 { 205 if((bytes[j]&0xff)!==(opcode[j]&0xff)) 206 { 207 match = false; 208 break; 209 } 210 } 211 if(match && record.text) out.push(record.text); 212 } 213 return out.join(" "); 214 } 215
216 function invalidateDecodeCache() 217 { 218 decodeCache = new Array(0x10000); 219 } 220 221 function init(cfg) 222 { 223 var cpu = liveCPU(); 224 if(!cpu) return false; 225 226 cpu_config = cpu.getConfig(); 227 max_instrlen = Math.max.apply(null,cpu_config.instrlen); 228 if(cfg && cfg.scrollH) listingRows = cfg.scrollH; 229 230 oDASM_debug = new DASM(); 231 oDASM_debug.getHexByte = oCOM.getHexByte; 232 oDASM_debug.sym_search = function(op,adm) 233 { 234 var opd = parseInt(op.substring(1,op.length),16); 235 236 switch(adm) 237 { 238 case "zpg": 239 case "zpx": 240 case "zpy": 241 case "abs": 242 case "abx": 243 case "aby": 244 case "rel": 245 case "ind": 246 case "iny": 247 case "inx": 248 var name = symbolAtValue(opd); 249 if(name) return name+" <small>"+op+"h</small>"; 250 name = symbolAtValue((opd-1)&0xffff); 251 if(name) return name+"+1 <small>"+op+"h</small>"; 252 break; 253 } 254 return op; 255 }; 256 return true; 257 } 258 259 function ensureInit(cfg) 260 { 261 return cpu_config!==null || init(cfg); 262 } 263 264 function stripMarkup(text) 265 { 266 var sanitized = String(text==null ? "" : text) 267 .replace(/ /gi," ") 268 .replace(/</gi,"<") 269 .replace(/>/gi,">") 270 .replace(/&/gi,"&"); 271 272 var previous; 273 do { 274 previous = sanitized; 275 sanitized = sanitized.replace(/<[^>]*>/g,""); 276 } while(sanitized!==previous); 277 278 return sanitized.replace(/\s+/g," ").trim(); 279 } 280 281 function decodeAt(addr) 282 { 283 if(!ensureInit()) return null; 284 285 addr &= 0xffff; 286 var b0 = traceRead(addr); 287 if(b0===null) return null; 288 var b1 = traceRead((addr+1)&0xffff); 289 var b2 = traceRead((addr+2)&0xffff); 290 if(b1===null) b1 = 0x00; 291 if(b2===null) b2 = 0x00; 292 var cached = decodeCache[addr]; 293 294 if(cached && cached.b0===b0 && cached.b1===b1 && cached.b2===b2) 295 { 296 cacheHits++; 297 return cached; 298 } 299 300 if(cached && previousBoundary[cached.next]===addr) 301 previousBoundary[cached.next] = -1; 302 303 cacheMisses++; 304 var len = cpu_config.instrlen[b0] || 1; 305 var entry = cpu_config.opctab[b0] || ["???","imp"]; 306 var adm = entry[1] || "imp"; 307 var val = null; 308 309 switch(adm) 310 { 311 case "rel": 312 var delta = (b1&0x80) ? b1-0x100 : b1; 313 val = (addr+2+delta)&0xffff; 314 break; 315 case "abs": 316 case "abx": 317 case "aby": 318 case "ind": 319 val = (b1 | (b2<<8))&0xffff; 320 break; 321 case "zpg": 322 case "zpx": 323 case "zpy": 324 case "inx": 325 case "iny": 326 val = b1&0xff; 327 break; 328 } 329 330 var ret = oDASM_debug.disassemble({ 331 "code_arr":[b0,b1,b2] 332 ,"pc":addr 333 ,"opctab":cpu_config.opctab 334 }); 335 var mnemonic = stripMarkup(ret.mnemonic); 336 var match = /^(\S+)(?:\s+(.*))?$/.exec(mnemonic) || ["",mnemonic,""]; 337 var bytes = [b0,b1,b2].slice(0,len).map(function(v){return oCOM.getHexByte(v)}).join(" "); 338 339 cached = { 340 "addr":addr 341 ,"b0":b0,"b1":b1,"b2":b2 342 ,"len":len 343 ,"next":(addr+len)&0xffff 344 ,"bytes":bytes 345 ,"ins":match[1] || "" 346 ,"opr":match[2] || "" 347 ,"adm":adm 348 ,"val":val 349 }; 350 decodeCache[addr] = cached; 351 return cached; 352 } 353 354 function safeWord(addr) 355 { 356 addr &= 0xffff; 357 var lo = traceRead(addr); 358 var hi = traceRead((addr+1)&0xffff); 359 if(lo===null || hi===null) return null; 360 return (lo | (hi<<8)) & 0xffff; 361 } 362 363 function parseAddress(value) 364 { 365 var s = String(value==null ? "" : value).trim(); 366 s = s.replace(/^\$/,"").replace(/^0x/i,""); 367 if(!/^[0-9a-f]{1,4}$/i.test(s)) return null; 368 return parseInt(s,16) & 0xffff; 369 } 370 371 function parseSymbolValue(value) 372 { 373 if(typeof(value)==="number" && Number.isFinite(value)) return value&0xffff; 374 var s = String(value==null ? "" : value).trim(); 375 if(/^\$[0-9a-f]+$/i.test(s)) return parseInt(s.substring(1),16)&0xffff; 376 if(/^0x[0-9a-f]+$/i.test(s)) return parseInt(s.substring(2),16)&0xffff; 377 if(/^[0-9]+$/.test(s)) return parseInt(s,10)&0xffff; 378 if(/^[0-9a-f]+$/i.test(s)) return parseInt(s,16)&0xffff; 379 return null; 380 } 381 382 function parseSymbolByte(value) 383 { 384 if(typeof(value)==="number" && Number.isFinite(value)) return value&0xff;
385 var s = String(value==null ? "" : value).trim(); 386 if(/^\$[0-9a-f]{1,2}$/i.test(s)) return parseInt(s.substring(1),16)&0xff; 387 if(/^0x[0-9a-f]{1,2}$/i.test(s)) return parseInt(s.substring(2),16)&0xff; 388 if(/^[0-9a-f]{1,2}$/i.test(s)) return parseInt(s,16)&0xff; 389 return null; 390 } 391 392 function commitLoadedSymbols(nextLabels,nextSymbols,nextComments,nextNames,state) 393 { 394 loadedLabels = nextLabels; 395 loadedSymbols = nextSymbols; 396 loadedComments = nextComments; 397 loadedSymbolNames = nextNames || Object.create(null); 398 symbolState = state; 399 invalidateDecodeCache(); 400 syncSymbolControls(); 401 if(currentPC!==null) renderListing(currentPC,true); 402 return Object.assign({},symbolState); 403 } 404 405 function loadSymbolObject(raw,fileName) 406 { 407 var records = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.symbols) ? raw.symbols : null); 408 if(!records) throw new Error("Symbol JSON must contain a symbols array"); 409 410 var nextLabels = new Array(0x10000); 411 var nextSymbols = new Array(0x10000); 412 var nextComments = new Array(0x10000); 413 var nextNames = Object.create(null); 414 var labels = 0; 415 var equs = 0; 416 var comments = 0; 417 418 for(var i=0;i<records.length;i++) 419 { 420 var record = records[i] || {}; 421 var type = String(record.type || "label").toLowerCase(); 422 var value = parseSymbolValue(record.value!==undefined ? record.value : record.address); 423 if(value===null) continue; 424 425 if(type==="label") 426 { 427 var label = String(record.name || "").trim(); 428 if(!label) continue; 429 if(!nextLabels[value]) nextLabels[value] = label; 430 nextSymbols[value] = label; 431 nextNames[label.toUpperCase()] = {name:label,value:value,type:"label"}; 432 labels++; 433 continue; 434 } 435 436 if(type==="equ" || type==="symbol") 437 { 438 var name = String(record.name || "").trim(); 439 if(!name) continue; 440 if(!nextSymbols[value]) nextSymbols[value] = name; 441 nextNames[name.toUpperCase()] = {name:name,value:value,type:type==="symbol" ? "equ" : type}; 442 equs++; 443 continue; 444 } 445 446 if(type==="comment" && (!record.targetType || String(record.targetType).toLowerCase()==="instruction")) 447 { 448 var text = String(record.text || record.comment || "").trim(); 449 if(!text) continue; 450 var opcode = []; 451 if(Array.isArray(record.opcode)) 452 { 453 for(var b=0;b<record.opcode.length;b++) 454 { 455 var parsed = parseSymbolByte(record.opcode[b]); 456 if(parsed===null) { opcode = []; break; } 457 opcode.push(parsed); 458 } 459 } 460 if(!nextComments[value]) nextComments[value] = []; 461 nextComments[value].push({text:text,opcode:opcode}); 462 comments++; 463 } 464 } 465 466 if(labels+equs+comments===0) throw new Error("No usable labels, EQU symbols or instruction comments found"); 467 468 var fmt = raw && !Array.isArray(raw) && raw.format ? String(raw.format) : "JSON symbols"; 469 if(fmt!=="RetroAppleJS-ASM-symbols" && raw && !Array.isArray(raw) && raw.format) 470 throw new Error("Unsupported symbol table format: "+fmt); 471 472 return commitLoadedSymbols(nextLabels,nextSymbols,nextComments,nextNames,{ 473 file:String(fileName || "symbols.json") 474 ,source:raw && !Array.isArray(raw) ? String(raw.sourceName || "") : "" 475 ,format:fmt 476 ,labels:labels 477 ,equs:equs 478 ,comments:comments 479 ,error:"" 480 }); 481 } 482 483 function loadSymbolTextMap(text,fileName) 484 { 485 var nextLabels = new Array(0x10000); 486 var nextSymbols = new Array(0x10000); 487 var nextComments = new Array(0x10000); 488 var nextNames = Object.create(null); 489 var labels = 0; 490 var lines = String(text || "").split(/\r?\n/); 491 492 function hexAddress(token) 493 { 494 token = String(token || "").trim(); 495 if(/^\$[0-9a-f]{1,4}$/i.test(token)) return parseInt(token.substring(1),16)&0xffff; 496 if(/^0x[0-9a-f]{1,4}$/i.test(token)) return parseInt(token.substring(2),16)&0xffff; 497 if(/^[0-9a-f]{1,4}$/i.test(token)) return parseInt(token,16)&0xffff; 498 return null; 499 } 500 501 for(var i=0;i<lines.length;i++) 502 { 503 var line = lines[i].replace(/;.*/,"").trim(); 504 if(!line) continue; 505 var m = /^([A-Za-z_.$@?][\w.$@?]*)\s*(?:=|EQU)\s*(\$?[0-9A-Fa-f]{1,4}|0x[0-9A-Fa-f]{1,4})$/i.exec(line); 506 var name,addr; 507 if(m) 508 { 509 name = m[1]; 510 addr = hexAddress(m[2]); 511 } 512 else 513 { 514 m = /^(\$?[0-9A-Fa-f]{1,4}|0x[0-9A-Fa-f]{1,4})\s+([A-Za-z_.$@?][\w.$@?]*)$/.exec(line); 515 if(m) { addr = hexAddress(m[1]); name = m[2]; } 516 else 517 { 518 m = /^([A-Za-z_.$@?][\w.$@?]*)\s+(\$?[0-9A-Fa-f]{1,4}|0x[0-9A-Fa-f]{1,4})$/.exec(line); 519 if(m) { name = m[1]; addr = hexAddress(m[2]); } 520 } 521 } 522 if(addr===null || addr===undefined || !name) continue; 523 if(!nextLabels[addr]) nextLabels[addr] = name; 524 if(!nextSymbols[addr]) nextSymbols[addr] = name; 525 nextNames[String(name).toUpperCase()] = {name:String(name),value:addr,type:"label"}; 526 labels++; 527 } 528 529 if(!labels) throw new Error("No symbols found in text map");
530 return commitLoadedSymbols(nextLabels,nextSymbols,nextComments,nextNames,{ 531 file:String(fileName || "symbols.txt") 532 ,source:"" 533 ,format:"text symbols" 534 ,labels:labels 535 ,equs:0 536 ,comments:0 537 ,error:"" 538 }); 539 } 540 541 function resetLoadedSymbols() 542 { 543 loadedLabels = new Array(0x10000); 544 loadedSymbols = new Array(0x10000); 545 loadedComments = new Array(0x10000); 546 loadedSymbolNames = Object.create(null); 547 symbolState = {file:"",source:"",format:"",labels:0,equs:0,comments:0,error:""}; 548 invalidateDecodeCache(); 549 syncSymbolControls(); 550 if(currentPC!==null) renderListing(currentPC,true); 551 return true; 552 } 553 554 function tokenizeCondition(text) 555 { 556 var s = String(text==null ? "" : text); 557 var out = []; 558 var i = 0; 559 560 function fail(message,pos) 561 { 562 throw new Error(message+" at column "+((pos===undefined ? i : pos)+1)); 563 } 564 565 while(i<s.length) 566 { 567 var c = s.charAt(i); 568 if(/\s/.test(c)) { i++; continue; } 569 570 var two = s.substr(i,2); 571 if(["&&","||","==","!=","<=",">="].indexOf(two)>=0) 572 { 573 out.push({k:"op",v:two,p:i}); 574 i += 2; 575 continue; 576 } 577
578 if("()[]&|^!<>= ".indexOf(c)>=0 && c!==" ") 579 { 580 out.push({k:"op",v:c,p:i}); 581 i++; 582 continue; 583 } 584 585 if(c==="$") 586 { 587 var hp = i++; 588 var hs = ""; 589 while(i<s.length && /[0-9a-f]/i.test(s.charAt(i))) hs += s.charAt(i++); 590 if(!hs) fail("Expected hexadecimal digits",hp); 591 out.push({k:"num",v:parseInt(hs,16),p:hp}); 592 continue; 593 } 594 595 if(c==="0" && i+1<s.length && /[xX]/.test(s.charAt(i+1))) 596 { 597 var xp = i; 598 i += 2; 599 var xs = ""; 600 while(i<s.length && /[0-9a-f]/i.test(s.charAt(i))) xs += s.charAt(i++); 601 if(!xs) fail("Expected hexadecimal digits",xp); 602 out.push({k:"num",v:parseInt(xs,16),p:xp}); 603 continue; 604 } 605 606 if(/[0-9]/.test(c)) 607 { 608 var np = i; 609 var ns = ""; 610 while(i<s.length && /[0-9]/.test(s.charAt(i))) ns += s.charAt(i++); 611 out.push({k:"num",v:parseInt(ns,10),p:np}); 612 continue; 613 } 614 615 if(/[a-z_.@?]/i.test(c)) 616 { 617 var ip = i; 618 var id = ""; 619 while(i<s.length && /[a-z0-9_.@$?]/i.test(s.charAt(i))) id += s.charAt(i++); 620 out.push({k:"id",v:id.toUpperCase(),p:ip}); 621 continue; 622 } 623 624 fail("Unexpected character '"+c+"'",i); 625 } 626 627 out.push({k:"eof",v:"",p:i}); 628 return out; 629 } 630 631 function parseBreakpointCondition(text) 632 { 633 var tokens = tokenizeCondition(text); 634 var pos = 0; 635 var registers = {A:1,X:1,Y:1,SP:1,P:1,PC:1,INS:1}; 636 var flags = {N:1,V:1,B:1,D:1,I:1,Z:1,C:1}; 637 var memory = {M:8,M8:8,MEM:8,MEM8:8,M16:16,MEM16:16}; 638 639 function token(){ return tokens[pos]; } 640 function take(value) 641 { 642 if(token().v===value) { pos++; return true; } 643 return false; 644 } 645 function need(value) 646 { 647 if(!take(value)) throw new Error("Expected '"+value+"' at column "+(token().p+1)); 648 } 649 650 function primary() 651 { 652 var t = token(); 653 if(t.k==="num") 654 { 655 pos++; 656 return {t:"num",v:t.v}; 657 } 658 659 if(t.k==="id") 660 { 661 pos++; 662 if(t.v==="TRUE") return {t:"num",v:1}; 663 if(t.v==="FALSE") return {t:"num",v:0}; 664 if(registers[t.v]) return {t:"reg",v:t.v}; 665 if(flags[t.v]) return {t:"flag",v:t.v}; 666 if(memory[t.v]) 667 { 668 need("["); 669 var addr = logicalOr(); 670 need("]"); 671 return {t:"mem",w:memory[t.v],a:addr}; 672 } 673 var symbol = loadedSymbolNames[t.v]; 674 if(symbol) return {t:"num",v:symbol.value}; 675 throw new Error("Unknown condition name '"+t.v+"' at column "+(t.p+1)); 676 } 677 678 if(take("(")) 679 { 680 var n = logicalOr(); 681 need(")"); 682 return n; 683 } 684 685 throw new Error("Expected value at column "+(t.p+1)); 686 } 687 688 function unary() 689 { 690 if(take("!")) return {t:"un",o:"!",a:unary()}; 691 return primary(); 692 } 693 694 function bitAnd() 695 { 696 var n = unary(); 697 while(take("&")) n = {t:"bin",o:"&",a:n,b:unary()}; 698 return n; 699 } 700 701 function bitXor() 702 { 703 var n = bitAnd(); 704 while(take("^")) n = {t:"bin",o:"^",a:n,b:bitAnd()}; 705 return n; 706 } 707 708 function bitOr() 709 { 710 var n = bitXor(); 711 while(take("|")) n = {t:"bin",o:"|",a:n,b:bitXor()}; 712 return n; 713 } 714 715 function compare() 716 { 717 var n = bitOr(); 718 while(["=","==","!=","<","<=",">",">="].indexOf(token().v)>=0) 719 { 720 var op = token().v; 721 pos++; 722 n = {t:"bin",o:op,a:n,b:bitOr()}; 723 } 724 return n; 725 } 726 727 function logicalAnd() 728 { 729 var n = compare(); 730 while(take("&&")) n = {t:"bin",o:"&&",a:n,b:compare()}; 731 return n; 732 } 733 734 function logicalOr() 735 { 736 var n = logicalAnd(); 737 while(take("||")) n = {t:"bin",o:"||",a:n,b:logicalAnd()}; 738 return n; 739 } 740 741 var ast = logicalOr(); 742 if(token().k!=="eof") 743 throw new Error("Unexpected token '"+token().v+"' at column "+(token().p+1)); 744 return ast; 745 } 746 747 function conditionValue(node,state) 748 { 749 if(!node) return 1; 750 751 if(node.t==="num") return Number(node.v) || 0; 752 753 if(node.t==="reg") 754 { 755 var key = node.v==="INS" ? "ic" : node.v.toLowerCase(); 756 if(!state || state[key]===undefined) throw new Error("Register/counter "+node.v+" is unavailable"); 757 return Number(state[key]) || 0; 758 } 759 760 if(node.t==="flag") 761 { 762 if(!state || state.p===undefined) throw new Error("Processor flags are unavailable"); 763 var masks = {N:0x80,V:0x40,B:0x10,D:0x08,I:0x04,Z:0x02,C:0x01}; 764 return (Number(state.p) & masks[node.v]) ? 1 : 0; 765 } 766 767 if(node.t==="mem") 768 { 769 var addr = conditionValue(node.a,state) & 0xffff; 770 var lo = traceRead(addr); 771 if(lo===null) 772 throw new Error("Debugger read is masked at $"+oCOM.getHexWord(addr)); 773 if(node.w===8) return lo; 774 var hiAddr = (addr+1)&0xffff; 775 var hi = traceRead(hiAddr); 776 if(hi===null) 777 throw new Error("Debugger read is masked at $"+oCOM.getHexWord(hiAddr)); 778 return lo | ((hi & 0xff)<<8); 779 } 780 781 if(node.t==="un") return conditionValue(node.a,state) ? 0 : 1; 782 783 if(node.t==="bin") 784 { 785 if(node.o==="&&") return conditionValue(node.a,state) ? (conditionValue(node.b,state) ? 1 : 0) : 0; 786 if(node.o==="||") return conditionValue(node.a,state) ? 1 : (conditionValue(node.b,state) ? 1 : 0); 787 788 var a = conditionValue(node.a,state); 789 var b = conditionValue(node.b,state); 790 switch(node.o) 791 { 792 case "&": return ((a|0)&(b|0))>>>0; 793 case "|": return ((a|0)|(b|0))>>>0; 794 case "^": return ((a|0)^(b|0))>>>0; 795 case "=": 796 case "==": return a===b ? 1 : 0; 797 case "!=": return a!==b ? 1 : 0; 798 case "<": return a<b ? 1 : 0; 799 case "<=": return a<=b ? 1 : 0; 800 case ">": return a>b ? 1 : 0; 801 case ">=": return a>=b ? 1 : 0; 802 } 803 } 804 805 throw new Error("Invalid condition expression"); 806 } 807 808 function compileBreakpointCondition(text) 809 { 810 text = String(text==null ? "" : text).trim();
811 return {text:text,ast:text ? parseBreakpointCondition(text) : null}; 812 } 813 814 // A PC equality inside an AND-only path is a safe address gate for the full 815 // expression. It keeps common address breakpoints off the every-instruction 816 // condition path without reintroducing a second user-facing breakpoint type. 817 function breakpointPcGate(node) 818 { 819 if(!node) return null; 820 821 if(node.t==="bin" && node.o==="&&") 822 { 823 var left = breakpointPcGate(node.a); 824 var right = breakpointPcGate(node.b); 825 if(left!==null && right!==null && left!==right) return null; 826 return left!==null ? left : right; 827 } 828 829 if(node.t==="bin" && (node.o==="=" || node.o==="==")) 830 { 831 if(node.a && node.a.t==="reg" && node.a.v==="PC" && node.b && node.b.t==="num") 832 return Number(node.b.v) & 0xffff; 833 if(node.b && node.b.t==="reg" && node.b.v==="PC" && node.a && node.a.t==="num") 834 return Number(node.a.v) & 0xffff; 835 } 836 return null; 837 } 838 839 function parseColumns(text) 840 { 841 var out = {}; 842 String(text || "").replace(/([a-zA-Z_][\w]*)\s*:\s*(-?\d+)/g,function(_,key,val) 843 { 844 out[key] = Math.max(0,parseInt(val,10) || 0); 845 return ""; 846 }); 847 return out; 848 } 849 850 function crop(text,width) 851 { 852 text = String(text==null ? "" : text); 853 if(width<=0) return ""; 854 if(text.length<=width) return text; 855 if(width===1) return text.slice(0,1); 856 return text.slice(0,width-1)+"…"; 857 } 858 859 function formatParts(parts) 860 { 861 var columns = parseColumns(listingColumns); 862 var order = ["adr","code","lin","lbl","ins","opr","com"]; 863 var active = order.filter(function(k){return Object.prototype.hasOwnProperty.call(columns,k)}); 864 active.sort(function(a,b){return columns[a]-columns[b]}); 865 866 var line = ""; 867 for(var i=0;i<active.length;i++) 868 { 869 var key = active[i]; 870 var col = columns[key]; 871 while(line.length<col) line += " "; 872 873 var nextCol = i+1<active.length ? columns[active[i+1]] : null; 874 var value = String(parts[key]==null ? "" : parts[key]); 875 if(nextCol!==null) 876 { 877 // ASM_core's branch-line renderer owns the complete interval 878 // [lin,nextColumn): its rightmost character is the ▶ arrowhead. 879 // Other fields retain the traditional one-column separator. 880 var width = nextCol-col-(key==="lin" ? 0 : 1); 881 value = crop(value,Math.max(0,width)); 882 } 883 line += value; 884 } 885 return line.replace(/\s+$/g,""); 886 } 887 888 function formatDecoded(d,lin,lbl,com) 889 { 890 return formatParts({ 891 "adr":oCOM.getHexWord(d.addr)+":" 892 ,"code":d.bytes 893 ,"lin":lin || "" 894 ,"lbl":lbl || "" 895 ,"ins":d.ins 896 ,"opr":d.opr 897 ,"com":com || "" 898 }); 899 } 900 901 function rememberSequential(from,to) 902 { 903 if(from==null || to==null) return false; 904 from &= 0xffff; 905 to &= 0xffff; 906 var d = decodeAt(from); 907 if(!d || d.next!==to) return false; 908 previousBoundary[to] = from; 909 return true; 910 } 911 912 function rememberEdges(edges) 913 { 914 if(!Array.isArray(edges)) return; 915 for(var i=0;i<edges.length;i++) 916 if(edges[i] && edges[i].length>=2) 917 rememberSequential(edges[i][0],edges[i][1]); 918 } 919 920 function predecessorOf(addr) 921 { 922 addr &= 0xffff; 923 var known = previousBoundary[addr]; 924 925 if(known>=0) 926 { 927 var kd = decodeAt(known); 928 if(kd && kd.next===addr) return known; 929 previousBoundary[addr] = -1; 930 } 931 932 // Backward 6502 decoding is ambiguous in general. Accept an inferred 933 // predecessor only when exactly one 1..3-byte candidate lands here. 934 var found = -1; 935 var count = 0; 936 for(var delta=1;delta<=max_instrlen;delta++) 937 { 938 var candidate = (addr-delta)&0xffff; 939 var d = decodeAt(candidate); 940 if(d && d.next===addr) 941 { 942 found = candidate; 943 count++; 944 } 945 } 946 return count===1 ? found : -1; 947 } 948 949 function applyListingDecorations(rows) 950 { 951 rows = rows || []; 952 var temp = []; 953 954 for(var i=0;i<rows.length;i++) 955 { 956 var d = rows[i].decoded; 957 temp.push({ 958 bytes:[d.b0,d.b1,d.b2].slice(0,d.len) 959 ,pc:d.addr 960 ,mnemonic:d.ins 961 ,addrMode:d.adm 962 ,val:d.val 963 ,lin:"" 964 }); 965 } 966 967 // Reuse the assembler's established Unicode branch-guide algorithm. 968 // It draws relative branches and absolute JMPs only when both endpoints 969 // are present in the current visible instruction window. 970 if(typeof(ASM)==="function") 971 { 972 if(!oListingASM) oListingASM = new ASM(); 973 if(oListingASM && typeof(oListingASM.applyListingLineColumn)==="function") 974 { 975 oListingASM.listingColumns = parseColumns(listingColumns); 976 oListingASM.applyListingLineColumn(temp); 977 } 978 } 979 980 for(var r=0;r<rows.length;r++) 981 { 982 var decoded = rows[r].decoded; 983 rows[r].lin = temp[r] && temp[r].lin ? temp[r].lin : ""; 984 rows[r].lbl = labelAtAddress(decoded.addr); 985 rows[r].com = commentAtAddress(decoded.addr,decoded); 986 rows[r].text = formatDecoded(decoded,rows[r].lin,rows[r].lbl,rows[r].com); 987 } 988 return rows; 989 } 990 991 function forwardWindow(start,pc,count) 992 { 993 start &= 0xffff; 994 pc &= 0xffff; 995 count = Math.max(1,Number(count)|0 || listingRows); 996 997 var rows = []; 998 var adr = start; 999 for(var row=0;row<count;row++) 1000 { 1001 var d = decodeAt(adr); 1002 if(!d) break; 1003 1004 rows.push({ 1005 "addr":adr 1006 ,"current":adr===pc 1007 ,"decoded":d 1008 ,"text":"" 1009 }); 1010 1011 previousBoundary[d.next] = adr; 1012 adr = d.next; 1013 } 1014 return applyListingDecorations(rows); 1015 } 1016 1017 function followWindow(pc,count) 1018 { 1019 pc &= 0xffff; 1020 count = Math.max(1,Number(count)|0 || listingRows); 1021 var aboveWanted = Math.min(8,Math.floor(count/2)); 1022 var start = pc; 1023 1024 for(var i=0;i<aboveWanted;i++) 1025 { 1026 var p = predecessorOf(start); 1027 if(p<0) break; 1028 start = p; 1029 } 1030 return forwardWindow(start,pc,count); 1031 } 1032 1033 function listingWindow(pc,count) 1034 { 1035 return followPC || manualTop===null 1036 ? followWindow(pc,count) 1037 : forwardWindow(manualTop,pc,count); 1038 } 1039 1040 function ensureRowPool() 1041 { 1042 var el = document.getElementById(dbg.body_id); 1043 if(!el) return null; 1044 1045 var pool = el._cpuDbgRowPool; 1046 if(pool && pool.length===listingRows && pool.length && pool[0].parentNode===el) 1047 return pool; 1048 1049 el.innerHTML = ""; 1050 pool = new Array(listingRows); 1051 for(var i=0;i<listingRows;i++) 1052 { 1053 var row = document.createElement("div"); 1054 row.style.height = rowPixelHeight+"px"; 1055 row.style.lineHeight = rowPixelHeight+"px"; 1056 row.style.whiteSpace = "pre"; 1057 row.style.fontFamily = "inherit"; 1058 row.style.fontKerning = "none"; 1059 row.style.fontVariantLigatures = "none"; 1060 row.style.cursor = "default"; 1061 row._cpuDbgText = null; 1062 row._cpuDbgCurrent = null; 1063 row._cpuDbgAddr = null; 1064 el.appendChild(row); 1065 pool[i] = row; 1066 } 1067 el._cpuDbgRowPool = pool; 1068 return pool; 1069 } 1070 1071 function syncFollowControl() 1072 { 1073 var icon = document.getElementById("cpuDbg_trackPc"); 1074 if(!icon) return; 1075 icon.classList.toggle("fa-lock",followPC); 1076 icon.classList.toggle("fa-lock-open",!followPC); 1077 icon.style.opacity = "1"; 1078 icon.setAttribute("aria-pressed",followPC ? "true" : "false"); 1079 icon.title = followPC 1080 ? "Track PC enabled — click to unlock the listing" 1081 : "Track PC disabled — click to lock the listing to the live PC"; 1082 } 1083 1084 function syncLoopControl() 1085 { 1086 var icon = document.getElementById("cpuDbg_showLoopSteps"); 1087 if(!icon) return; 1088 var skipperEnabled = !showLoopSteps; 1089 icon.style.opacity = skipperEnabled ? "1" : ".32"; 1090 icon.setAttribute("aria-pressed",skipperEnabled ? "true" : "false"); 1091 icon.title = skipperEnabled 1092 ? "Closed-loop step skipper enabled — proven loops run without IPS delay; click to disable" 1093 : "Closed-loop step skipper disabled — click to accelerate proven closed loops"; 1094 } 1095
1096 function resetClosedLoopDisplayState() 1097 { 1098 activeClosedLoop = null; 1099 } 1100 1101 function closedLoopSkipperActive() 1102 { 1103 return !showLoopSteps && activeClosedLoop!==null; 1104 } 1105 1106 function loopContains(loop,addr) 1107 { 1108 addr &= 0xffff; 1109 return !!loop && addr>=loop.lo && addr<=loop.hi; 1110 } 1111 1112 function isTakenBackwardLoopEdge(d,from,to) 1113 { 1114 if(!d) return false; 1115 from &= 0xffff; 1116 to &= 0xffff; 1117 // Restrict loop discovery to actual taken relative branches and JMPs. 1118 // RTS/RTI/call returns may move backwards too, but are not proof of a 1119 // closed loop and therefore must never trigger display suppression. 1120 if(to>from) return false; 1121 if(d.adm==="rel") 1122 return d.val!==null && d.val!==undefined && to===(d.val&0xffff); 1123 // The live endPC is authoritative for JMP, including JMP (indirect), 1124 // whose decoded operand value is the pointer rather than its destination. 1125 return d.ins==="JMP"; 1126 } 1127 1128 function observeClosedLoopEdge(from,to) 1129 { 1130 from &= 0xffff; 1131 to &= 0xffff; 1132 if(showLoopSteps) return {suppress:false,entered:false,exited:false}; 1133 1134 var d = decodeAt(from); 1135 if(!d) 1136 { 1137 if(activeClosedLoop) 1138 { 1139 resetClosedLoopDisplayState(); 1140 loopDisplayStats.exits++; 1141 return {suppress:false,entered:false,exited:true}; 1142 } 1143 return {suppress:false,entered:false,exited:false}; 1144 } 1145 1146 if(!activeClosedLoop) 1147 { 1148 if(!isTakenBackwardLoopEdge(d,from,to)) 1149 return {suppress:false,entered:false,exited:false}; 1150 1151 activeClosedLoop = { 1152 target:to 1153 ,backPC:from 1154 ,lo:to 1155 ,hi:from 1156 ,callDepth:0 1157 ,iterations:1 1158 ,instructions:1 1159 }; 1160 loopDisplayStats.detected++; 1161 loopDisplayStats.hiddenInstructions++; 1162 return {suppress:true,entered:true,exited:false}; 1163 } 1164 1165 var loop = activeClosedLoop; 1166 loop.instructions++; 1167 loopDisplayStats.hiddenInstructions++; 1168 1169 // A subroutine called from inside the loop remains part of the hidden 1170 // iteration even when its address lies outside the loop's numeric range. 1171 // Nested JSR/RTS pairs are tracked so such calls do not look like exits. 1172 if(loop.callDepth>
11720) 1173 { 1174 if(d.b0===0x20) loop.callDepth++; // JSR 1175 else if(d.b0===0x60) loop.callDepth--; // RTS 1176 1177 if(loop.callDepth===0 && !loopContains(loop,to)) 1178 { 1179 resetClosedLoopDisplayState(); 1180 loopDisplayStats.exits++; 1181 return {suppress:false,entered:false,exited:true}; 1182 } 1183 return {suppress:true,entered:false,exited:false}; 1184 } 1185 1186 // An interrupt may temporarily take execution outside the numeric loop 1187 // range. Treat that as a visible escape rather than risk hiding unrelated 1188 // execution indefinitely; a later taken back-edge will prove the loop again. 1189 if(!loopContains(loop,from)) 1190 { 1191 if(loopContains(loop,to)) 1192 return {suppress:true,entered:false,exited:false}; 1193 resetClosedLoopDisplayState(); 1194 loopDisplayStats.exits++; 1195 return {suppress:false,entered:false,exited:true}; 1196 } 1197 1198 if(d.b0===0x20) 1199 { 1200 loop.callDepth = 1; 1201 return {suppress:true,entered:false,exited:false}; 1202 } 1203 1204 // The instruction that originally closed the loop is the definitive 1205 // iteration/exit boundary. A taken edge repeats; fall-through exits. 1206 if(from===loop.backPC) 1207 { 1208 if(to===loop.target) 1209 { 1210 loop.iterations++; 1211 return {suppress:true,entered:false,exited:false}; 1212 } 1213 resetClosedLoopDisplayState(); 1214 loopDisplayStats.exits++; 1215 return {suppress:false,entered:false,exited:true}; 1216 } 1217 1218 // Early branches/jumps out of the proven loop also end suppression. 1219 if(!loopContains(loop,to)) 1220 { 1221 resetClosedLoopDisplayState(); 1222 loopDisplayStats.exits++; 1223 return {suppress:false,entered:false,exited:true}; 1224 } 1225 1226 return {suppress:true,entered:false,exited:false}; 1227 } 1228 1229 function syncSymbolControls() 1230 { 1231 if(typeof(document)==="undefined") return; 1232 var status = document.getElementById("cpuDbg_symbolStatus"); 1233 var clear = document.getElementById("cpuDbg_symbolClear"); 1234 var total = symbolState.labels+symbolState.equs; 1235 var loaded = !!(symbolState.file || total || symbolState.comments); 1236 1237 if(status) 1238 { 1239 status.textContent = symbolState.error 1240 ? "error" 1241 : (loaded ? total+" sym / "+symbolState.comments+" com" : "none"); 1242 status.title = symbolState.error 1243 ? symbolState.error 1244 : (loaded 1245 ? (symbolState.file+(symbolState.source ? " · "+symbolState.source : "")+" · "+symbolState.labels+" labels, "+symbolState.equs+" EQU, "+symbolState.comments+" comments") 1246 : "No external symbol table loaded"); 1247 } 1248 if(clear) clear.disabled = !loaded; 1249 } 1250 1251 function syncBreakpointControls() 1252 { 1253 var cond = document.getElementById("cpuDbg_breakCond"); 1254 if(cond) 1255 { 1256 if(document.activeElement!==cond && cond.value!==breakConditionText) cond.value = breakConditionText; 1257 cond.style.borderColor = breakConditionError ? "#c00" : ""; 1258 cond.setAttribute("aria-invalid",breakConditionError ? "true" : "false"); 1259 cond.title = breakConditionError ? "Invalid condition: "+breakConditionError : conditionHelp; 1260 } 1261 1262 var arm = document.getElementById("cpuDbg_breakArm"); 1263 if(arm) 1264 { 1265 arm.textContent = conditionalBreakpoint.armed ? "Disarm" : "Arm"; 1266 arm.setAttribute("aria-pressed",conditionalBreakpoint.armed ? "true" : "false"); 1267 arm.style.fontWeight = conditionalBreakpoint.armed ? "700" : ""; 1268 arm.title = conditionalBreakpoint.armed 1269 ? "Disarm conditional breakpoint (F9)" 1270 : "Arm conditional breakpoint (F9)"; 1271 } 1272 } 1273 1274 function boundaryActionText() 1275 { 1276 if(!boundaryAction) return ""; 1277 if(boundaryAction.type==="over")
1278 return " OVER→$"+oCOM.getHexWord(boundaryAction.returnPC); 1279 return " OUT J"+boundaryAction.jsrDepth+" I"+boundaryAction.irqDepth; 1280 } 1281 1282 function breakpointText() 1283 { 1284 // Armed/disarmed state is already conveyed by the Arm/Disarm button. 1285 // A successful hit is shown by the parking icon; textual diagnostics 1286 // are rendered in the BREAK IF status slot beside the editor. 1287 if(conditionalBreakpoint.error) return " BP!"; 1288 return breakMessage ? " "+breakMessage : ""; 1289 } 1290 1291 function instructionCounterText() 1292 { 1293 var cpu = liveCPU(); 1294 var state = cpu && typeof(cpu.watch)==="function" ? cpu.watch() : null; 1295 var value = state && state.ic!==undefined ? Math.floor(Number(state.ic)) : 0; 1296 if(!Number.isFinite(value) || value<0) value = 0; 1297 return value.toString(16).toUpperCase().padStart(12,"0").slice(-12); 1298 } 1299 1300 function updateNavigationStatus(pc) 1301 { 1302 var el = document.getElementById("cpuDbg_navStatus"); 1303 if(el) 1304 { 1305 var navText = "PC $"+oCOM.getHexWord(pc) 1306 +" INS $"+instructionCounterText(); 1307 if(el.value!==navText) el.value = navText; 1308 1309 var title = followPC 1310 ? "Listing tracks the live program counter; PC/INS text is read-only and copyable" 1311 : "Manual instruction-row view; enable Track PC to resume tracking; PC/INS text is read-only and copyable"; 1312 if(conditionalBreakpoint.condition) 1313 title += "; breakpoint condition: "+conditionalBreakpoint.condition; 1314 el.title = title; 1315 } 1316 1317 // Keep transient run state and breakpoint diagnostics next to BREAK IF, 1318 // where they relate to the condition editor rather than to PC/INS. 1319 var status = document.getElementById("cpuDbg_breakStatus"); 1320 if(status) 1321 { 1322 var statusText = (boundaryActionText()+breakpointText()).replace(/^\s+|\s+$/g,""); 1323 status.textContent = statusText; 1324 status.style.display = statusText ? "" : "none"; 1325 status.title = statusText ? "Debugger run/breakpoint status: "+statusText : ""; 1326 } 1327 } 1328 1329 function updateRegisterStatus(state,force) 1330 { 1331 var el = document.getElementById(dbg.body_id+"_regs"); 1332 if(!el || !state || !oCOM || typeof(oCOM.formatCpuRegistersHTML)!=="function") return false; 1333 1334 var html = oCOM.formatCpuRegistersHTML(state,{includePC:false}); 1335 if(force || html!==lastRegisterHTML) 1336 { 1337 el.innerHTML = html; 1338 lastRegisterHTML = html; 1339 domWrites++; 1340 } 1341 return true; 1342 } 1343 1344 function renderListing(pc,force) 1345 { 1346 var pool = ensureRowPool(); 1347 if(!pool) return false; 1348 var rows = listingWindow(pc,listingRows); 1349 lastRows = rows; 1350 1351 lastViewTop = rows.length ? rows[0].addr : null; 1352 if(!followPC && manualTop===null && lastViewTop!==null) 1353 manualTop = lastViewTop; 1354 1355 for(var i=0;i<pool.length;i++) 1356 { 1357 var data = rows[i] || {text:"",current:false,addr:null}; 1358 var node = pool[i]; 1359 var text = data.text || ""; 1360 node._cpuDbgAddr = data.addr; 1361 node.title = data.addr==null ? "" : "Instruction $"+oCOM.getHexWord(data.addr); 1362 1363 if(force || node._cpuDbgText!==text) 1364 { 1365 node.textContent = text; 1366 node._cpuDbgText = text; 1367 domWrites++; 1368 } 1369 1370 if(force || node._cpuDbgCurrent!==!!data.current) 1371 { 1372 node.style.fontWeight = data.current ? "700" : "500"; 1373 node.style.textDecoration = data.current ? "underline" : "none"; 1374 node._cpuDbgCurrent = !!data.current; 1375 } 1376 1377 } 1378 1379 syncFollowControl(); 1380 syncLoopControl(); 1381 syncSymbolControls(); 1382 syncBreakpointControls(); 1383 updateNavigationStatus(pc); 1384 var cpu = liveCPU(); 1385 if(cpu) updateRegisterStatus(cpu.watch(),force); 1386 return true; 1387 } 1388
1389 function setFollowState(enabled,renderNow) 1390 { 1391 enabled = !!enabled; 1392 if(enabled) 1393 { 1394 followPC = true; 1395 manualTop = null; 1396 } 1397 else 1398 { 1399 if(followPC) 1400 { 1401 var cpu = liveCPU(); 1402 var pc = cpu ? (cpu.watch().pc & 0xffff) : (currentPC==null ? 0 : currentPC); 1403 if(lastViewTop===null) 1404 { 1405 var rows = followWindow(pc,listingRows); 1406 lastViewTop = rows.length ? rows[0].addr : pc; 1407 } 1408 manualTop = lastViewTop; 1409 } 1410 followPC = false; 1411 } 1412 1413 syncFollowControl(); 1414 if(renderNow && currentPC!==null) renderListing(currentPC,true); 1415 return followPC; 1416 } 1417 1418 function bindNavigation() 1419 { 1420 var el = document.getElementById(dbg.body_id); 1421 if(!el || el._cpuDbgNavigationBound) return; 1422 el._cpuDbgNavigationBound = true; 1423 el.tabIndex = 0; 1424 el.title = "Instruction navigation: wheel/↑/↓/PgUp/PgDn. F toggles Track PC; F9 breakpoint, F10 over, F11 step, Shift+F11 out."; 1425 1426 el.addEventListener("wheel",function(event) 1427 { 1428 event.preventDefault(); 1429 var page = event.shiftKey ? Math.max(1,listingRows-1) : 1; 1430 dbg.navigateRows(event.deltaY<0 ? -page : page); 1431 },{passive:false}); 1432 1433 el.addEventListener("keydown",function(event) 1434 { 1435 var handled = true; 1436 switch(event.key) 1437 {
1438 case "ArrowUp": dbg.navigateRows(-1); break; 1439 case "ArrowDown": dbg.navigateRows(1); break; 1440 case "PageUp": dbg.navigateRows(-Math.max(1,listingRows-1)); break; 1441 case "PageDown": dbg.navigateRows(Math.max(1,listingRows-1)); break; 1442 case "Home": dbg.setFollowPC(true); break; 1443 case "f": 1444 case "F": dbg.setFollowPC(!followPC); break; 1445 case "F9": 1446 if(event.shiftKey) dbg.clearConditionalBreakpoint(); 1447 else dbg.toggleConditionalBreakpointFromInput(); 1448 break; 1449 case "F10": dbg.stepOver(); break; 1450 case "F11": 1451 if(event.shiftKey) dbg.stepOut(); 1452 else dbg.step(); 1453 break; 1454 default: handled = false; 1455 } 1456 1457 if(handled) 1458 { 1459 event.preventDefault(); 1460 event.stopPropagation(); 1461 } 1462 }); 1463 1464 el.addEventListener("pointerdown",function(){ el.focus(); }); 1465 1466 el.addEventListener("touchstart",function(event) 1467 { 1468 if(!event.changedTouches || !event.changedTouches.length) return; 1469 el._cpuDbgTouchY = event.changedTouches[0].clientY; 1470 },{passive:true}); 1471 1472 el.addEventListener("touchmove",function(event) 1473 { 1474 if(!event.changedTouches || !event.changedTouches.length 1475 || !Number.isFinite(el._cpuDbgTouchY)) return; 1476 1477 var y = event.changedTouches[0].clientY; 1478 var delta = el._cpuDbgTouchY-y; 1479 var rows = delta<0 ? Math.ceil(delta/rowPixelHeight) : Math.floor(delta/rowPixelHeight); 1480 if(rows!==0) 1481 { 1482 event.preventDefault(); 1483 dbg.navigateRows(rows); 1484 el._cpuDbgTouchY = y; 1485 } 1486 },{passive:false}); 1487 1488 el.addEventListener("touchend",function(){ el._cpuDbgTouchY = null; },{passive:true}); 1489 el.addEventListener("touchcancel",function(){ el._cpuDbgTouchY = null; },{passive:true}); 1490 } 1491 1492 function syncBootTriggerInputs(state) 1493 { 1494 var start = document.getElementById("cpuDbg_bootStart"); 1495 var stop = document.getElementById("cpuDbg_bootStop"); 1496 var startText = state && state.triggerAddress!=null 1497 ? "$"+oCOM.getHexWord(state.triggerAddress) 1498 : ""; 1499 var stopText = state && state.stopAddress!=null 1500 ? "$"+oCOM.getHexWord(state.stopAddress) 1501 : ""; 1502 1503 // Do not fight the user while an address is being edited. 1504 if(start && document.activeElement!==start && start.value!==startText) start.value = startText; 1505 if(stop && document.activeElement!==stop && stop.value!==stopText) stop.value = stopText; 1506 } 1507 1508 function updateBootTriggerIcon(el,state) 1509 { 1510 syncBootTriggerInputs(state); 1511 if(!el) return; 1512 1513 if(state && state.bDebug_boot) 1514 { 1515 var startText = state.triggerAddress==null ? "immediately" : "$"+oCOM.getHexWord(state.triggerAddress); 1516 var stopText = state.stopAddress==null ? "when buffer is full" : "before $"+oCOM.getHexWord(state.stopAddress); 1517 var mode = state.triggerArmed ? "armed" 1518 : state.logging ? "logging" 1519 : state.full ? "buffer full" 1520 : state.complete ? "complete" 1521 : "enabled"; 1522 1523 el.style.opacity = "1"; 1524 el.title = "bootlog "+mode+"; start "+startText+"; stop "+stopText; 1525 } 1526 else 1527 { 1528 el.style.opacity = ".35"; 1529 el.title = "bootlog trigger disabled"; 1530 } 1531 } 1532 1533 function systemRunning() 1534 { 1535 return typeof(_o)!="undefined" 1536 && Number(_o.CPU_TargetTicks_s)>0 1537 && typeof(appleIntervalHandle)!="undefined" 1538 && appleIntervalHandle!=null; 1539 } 1540 1541 function executionRunning() 1542 { 1543 return fixedRunning || (runMode==="system" && systemRunning()) || !!boundaryAction; 1544 } 1545 1546 function rememberSystemSpeed() 1547 { 1548 if(typeof(_o)==="undefined") return; 1549 var base = Number(_o.CPU_ClocksTicks_s) || 1; 1550 var target = Number(_o.CPU_TargetTicks_s) || 0; 1551 if(target>0) resumePct = target/base; 1552 } 1553 1554 function pauseSystem() 1555 { 1556 if(systemRunning()) 1557 { 1558 rememberSystemSpeed(); 1559 1560 /* 1561 * Fixed-IPS STEP TRACE must become the sole live execution owner. 1562 * 1563 * Do NOT use oEMUI.cpuSpd(0) here. cpuSpd(0) clears the normal 1564 * SYSTEM interval and immediately creates another interval whose 1565 * CPU tick count is zero. That "paused" interval still calls 1566 * apple2plus.cycle(0), which in turn runs hw.io.cycle(), 1567 * dashboard work and Apple2Debug.cycle() while this fixed-IPS 1568 * scheduler is also active. 1569 * 1570 * Stop the SYSTEM timer itself instead. Keep the selected speed in 1571 * resumePct; resumeSystem() will recreate the normal SYSTEM timer. 1572 */ 1573 if(typeof(appleIntervalHandle)!=="undefined" 1574 && appleIntervalHandle!=null) 1575 { 1576 window.clearInterval(appleIntervalHandle); 1577 appleIntervalHandle = null; 1578 } 1579 } 1580 } 1581 1582 function resumeSystem() 1583 { 1584 oEMUI.cpuSpd(resumePct>0 ? resumePct : 1); 1585 } 1586 1587 function clearRunTimer() 1588 { 1589 if(runTimer!==null) 1590 { 1591 window.clearTimeout(runTimer); 1592 runTimer = null; 1593 } 1594 } 1595 1596 function stopFixedRun() 1597 { 1598 fixedRunning = false;
1599 fixedNextDueMs = null; 1600 clearRunTimer(); 1601 } 1602 1603 function fixedClockMs() 1604 { 1605 return typeof(performance)!=="undefined" && performance 1606 && typeof(performance.now)==="function" 1607 ? performance.now() 1608 : Date.now(); 1609 } 1610 1611 function stopBoundaryAction() 1612 { 1613 boundaryAction = null; 1614 clearRunTimer(); 1615 } 1616 1617 function runSpeedConfig(mode) 1618 { 1619 switch(String(mode)) 1620 { 1621 case "1": return {batch:1,delay:1000,label:"1 IPS"}; 1622 case "10": return {batch:1,delay:100,label:"10 IPS"}; 1623 case "100": return {batch:5,delay:50,label:"100 IPS"}; 1624 case "1000": return {batch:50,delay:50,label:"1000 IPS"}; 1625 default: return {batch:0,delay:0,label:"Max (SYSTEM)"}; 1626 } 1627 } 1628 1629 function boundarySpeedConfig() 1630 { 1631 if(runMode==="system") return {batch:250,delay:0}; 1632 return runSpeedConfig(runMode); 1633 } 1634 1635 function syncRunIcon(el) 1636 { 1637 el = el || document.getElementById("cpuDbg_play"); 1638 if(!el || !el.classList) return; 1639 var running = executionRunning(); 1640 var breakpointStop = !running && conditionalBreakpoint.hit && !conditionalBreakpoint.error; 1641 var stepPause = !running && !breakpointStop && manualStepPause;
1642 el.classList.toggle("fa-pause-circle",running || stepPause); 1643 el.classList.toggle("fa-parking",breakpointStop); 1644 el.classList.toggle("fa-play-circle",!running && !breakpointStop && !stepPause); 1645 el.title = running 1646 ? "pause CPU execution" 1647 : (breakpointStop ? "paused at BREAK IF condition — click to continue" : "continue CPU execution"); 1648 } 1649 1650 function scheduleFixedRun(delay) 1651 { 1652 clearRunTimer(); 1653 runTimer = window.setTimeout( 1654 fixedRunLoop, 1655 Math.max(0,Math.ceil(Number(delay) || 0)) 1656 ); 1657 } 1658 1659 /* 1660 * Fixed IPS is paced against an absolute wall-clock deadline. 1661 * 1662 * Do not schedule "delay ms after the previous batch": doing so adds CPU, 1663 * I/O and DOM-rendering time to every interval and causes cumulative drift. 1664 * 1665 * The first batch is due after one complete interval. Thus, measured from 1666 * pressing Run: 1667 * 1668 * 1 IPS -> 1 completed instruction after ~1 s 1669 * 10 IPS -> 10 completed instructions after ~1 s 1670 * 100 IPS -> 5 instructions every ~50 ms 1671 * 1000 IPS -> 50 instructions every ~50 ms 1672 */ 1673 function scheduleFirstFixedRun(cfg) 1674 { 1675 fixedNextDueMs = fixedClockMs()+cfg.delay; 1676 scheduleFixedRun(cfg.delay); 1677 } 1678 1679 function scheduleNextFixedRun(cfg) 1680 { 1681 var now = fixedClockMs(); 1682 fixedNextDueMs += cfg.delay; 1683 1684 // Browser suspension/background throttling can make a deadline very old. 1685 // Do not replay an arbitrarily large backlog in a burst. 1686 if(fixedNextDueMs < now-Math.max(1000,cfg.delay*4)) 1687 fixedNextDueMs = now+cfg.delay; 1688 1689 scheduleFixedRun(fixedNextDueMs-now); 1690 } 1691 1692 function fixedRunLoop() 1693 { 1694 if(!fixedRunning || runMode==="system") return; 1695 var machine = liveMachine(); 1696 if(!machine || typeof(machine.runLiveInstructionBatch)!="function") 1697 { 1698 stopFixedRun(); 1699 syncRunIcon(); 1700 return; 1701 } 1702 1703 var cfg = runSpeedConfig(runMode); 1704 var skippingClosedLoop = closedLoopSkipperActive(); 1705 var executionBatch = skippingClosedLoop ? closedLoopSkipBatch : cfg.batch; 1706 var renderAfterBatch = true; 1707 var batchOptions = {stopOnRegionChange:[0xc100,0xcfff]}; 1708 1709 if(!showLoopSteps) 1710 { 1711 renderAfterBatch = activeClosedLoop===null; 1712 batchOptions.onInstructionBoundary = function(one) 1713 { 1714 var decision = observeClosedLoopEdge(one.startPC,one.endPC); 1715 if(decision.suppress) 1716 { 1717 renderAfterBatch = false; 1718 return false; 1719 } 1720 if(decision.exited) 1721 { 1722 // Stop this cooperative batch exactly at the first boundary 1723 // outside the loop so the user sees the loop exit, not some 1724 // later sample up to 50 instructions further on. 1725 renderAfterBatch = true; 1726 return true; 1727 } 1728 renderAfterBatch = true; 1729 return false; 1730 }; 1731 } 1732 1733 // Normal execution uses the selected IPS batch. Once a taken backward 1734 // edge has proven a loop, subsequent loop instructions run in bounded 1735 // zero-delay bursts until the exact exit boundary. This accelerates the 1736 // boring loop without bypassing any CPU, I/O, breakpoint or video work. 1737 var result = machine.runLiveInstructionBatch(executionBatch,batchOptions); 1738 rememberEdges(result && result.edges); 1739 1740 // Do not render intermediate instructions while a proven loop is being 1741 // skipped. At the exact exit (or a breakpoint/stall) the normal debugger 1742 // render resumes; INS then shows the true number of executed opcodes. 1743 if(showLoopSteps || renderAfterBatch || !fixedRunning || !result || result.stalled) 1744 dbg.cycle({cpu:machine.cpuObj()}); 1745 1746 // A conditional breakpoint callback can stop fixedRunning from inside a 1747 // batch. Check ownership again before scheduling the next batch. 1748 if(!fixedRunning || !result || result.stalled) 1749 { 1750 stopFixedRun(); 1751 syncRunIcon(); 1752 return; 1753 } 1754 1755 if(closedLoopSkipperActive()) 1756 { 1757 // No selected-IPS delay while the proven loop remains active. A 1758 // bounded burst followed by setTimeout(0) keeps the browser responsive 1759 // even for a genuinely endless wait loop. 1760 scheduleFixedRun(0); 1761 return; 1762 } 1763 1764 // When a skip burst reaches the loop exit, restart the visible pacing 1765 // interval from that boundary instead of replaying an old deadline. 1766 if(skippingClosedLoop) fixedNextDueMs = fixedClockMs(); 1767 scheduleNextFixedRun(cfg); 1768 } 1769 1770 function startExecution() 1771 { 1772 // Leaving a breakpoint stop returns the main control from the 1773 // parking pictogram to the normal running state.
1774 conditionalBreakpoint.hit = false; 1775 manualStepPause = false; 1776 stopBoundaryAction(); 1777 if(runMode==="system") 1778 { 1779 resetClosedLoopDisplayState(); 1780 stopFixedRun(); 1781 resumeSystem(); 1782 } 1783 else 1784 { 1785 pauseSystem(); 1786 fixedRunning = true; 1787 scheduleFirstFixedRun(runSpeedConfig(runMode)); 1788 } 1789 syncRunIcon(); 1790 } 1791 1792 function pauseExecution() 1793 { 1794 stopBoundaryAction(); 1795 stopFixedRun(); 1796 if(runMode==="system") pauseSystem(); 1797 syncRunIcon(); 1798 } 1799 1800 function externalInterruptBoundary(before,one,opcode) 1801 { 1802 if(!before || !one || !one.state || opcode===0x00) return false; 1803 var spDrop = ((before.sp & 0xff)-(one.state.sp & 0xff)) & 0xff; 1804 if(spDrop!==3) return false; 1805 1806 var endPC = one.endPC & 0xffff; 1807 var nmi = safeWord(0xfffa); 1808 var irq = safeWord(0xfffe); 1809 return (nmi!==null && endPC===nmi) || (irq!==null && endPC===irq); 1810 } 1811 1812 function boundaryActionStopped(action,before,opcode,one) 1813 { 1814 if(action.type==="over") 1815 { 1816 return (one.endPC & 0xffff)===action.returnPC 1817 && (one.state.sp & 0xff)===action.startSP; 1818 } 1819 1820 if(opcode===0x00 || externalInterruptBoundary(before,one,opcode)) 1821 { 1822 action.irqDepth++; 1823 return false; 1824 } 1825 if(opcode===0x20) 1826 { 1827 action.jsrDepth++; 1828 return false; 1829 } 1830 if(opcode===0x60) 1831 { 1832 if(action.jsrDepth>0) 1833 { 1834 action.jsrDepth--; 1835 return false; 1836 } 1837 return action.irqDepth===0; 1838 } 1839 if(opcode===0x40) 1840 { 1841 if(action.irqDepth>0) 1842 { 1843 action.irqDepth--; 1844 return false; 1845 } 1846 return action.jsrDepth===0; 1847 } 1848 return false; 1849 } 1850 1851 function finishBoundaryAction() 1852 { 1853 stopBoundaryAction(); 1854 resetClosedLoopDisplayState(); 1855 var cpu = liveCPU(); 1856 if(cpu) dbg.cycle({cpu:cpu,force:true}); 1857 syncRunIcon(); 1858 } 1859 1860 function scheduleBoundaryAction(delay) 1861 { 1862 clearRunTimer(); 1863 runTimer = window.setTimeout(boundaryActionLoop,Math.max(0,delay|0)); 1864 } 1865 1866 function boundaryActionLoop() 1867 { 1868 var action = boundaryAction; 1869 var machine = liveMachine(); 1870 var cpu = liveCPU(); 1871 if(!action || !machine || !cpu || typeof(machine.stepLiveInstruction)!="function") 1872 { 1873 finishBoundaryAction(); 1874 return; 1875 } 1876 1877 var cfg = boundarySpeedConfig(); 1878 var skippingClosedLoop = closedLoopSkipperActive(); 1879 var executionBatch = skippingClosedLoop ? closedLoopSkipBatch : cfg.batch; 1880 var completed = 0; 1881 var stopped = false; 1882 var renderBoundaryProgress = showLoopSteps || activeClosedLoop===null; 1883 1884 for(var i=0;i<executionBatch && boundaryAction===action;i++) 1885 { 1886 var before = cpu.watch(); 1887 var startPC = before.pc & 0xffff; 1888 var decoded = decodeAt(startPC); 1889 var opcode = decoded ? decoded.b0 : null; 1890 var one = machine.stepLiveInstruction(); 1891 1892 // Cpu6502's execution trap returns a stalled instruction when a 1893 // conditional breakpoint is hit. The callback already cleared the 1894 // boundary action, so stop without executing the target opcode. 1895 if(!one || one.stalled || one.ticks<=0) break; 1896 1897 completed++; 1898 action.instructions++; 1899 rememberSequential(one.startPC,one.endPC); 1900 1901 // Completion of Step Over/Out has priority over a display-only 1902 // loop yield. Otherwise an RTS/return that is also the loop exit could 1903 // be followed by one unintended extra instruction on the next batch. 1904 if(boundaryActionStopped(action,before,opcode,one)) 1905 { 1906 stopped = true; 1907 break; 1908 } 1909 1910 if(!showLoopSteps) 1911 { 1912 var loopDecision = observeClosedLoopEdge(one.startPC,one.endPC); 1913 if(loopDecision.suppress) renderBoundaryProgress = false;
1914 else if(loopDecision.exited) 1915 { 1916 renderBoundaryProgress = true; 1917 // Yield at the exact loop exit before continuing Over/Out. 1918 break; 1919 } 1920 else renderBoundaryProgress = true; 1921 } 1922 } 1923 1924 // Intermediate instructions in a skipped loop are deliberately not 1925 // painted. The exact exit boundary (or Step Over/Out completion) is shown. 1926 if(showLoopSteps || renderBoundaryProgress || boundaryAction!==action || stopped || completed===0) 1927 dbg.cycle({cpu:cpu}); 1928 1929 if(boundaryAction!==action || stopped || completed===0) 1930 { 1931 finishBoundaryAction(); 1932 return; 1933 } 1934 if(closedLoopSkipperActive()) 1935 { 1936 scheduleBoundaryAction(0); 1937 return; 1938 } 1939 scheduleBoundaryAction(cfg.delay); 1940 } 1941 1942 function startBoundaryAction(action) 1943 { 1944 pauseExecution(); 1945 boundaryAction = action; 1946 syncRunIcon(); 1947 if(currentPC!==null) renderListing(currentPC,true); 1948 scheduleBoundaryAction(0); 1949 return action; 1950 } 1951 1952 function conditionalBreakpointHit(state,countHit) 1953 { 1954 manualStepPause = false; 1955 conditionalBreakpoint.armed = false; 1956 conditionalBreakpoint.error = conditionalBreakpoint.error || null; 1957 if(conditionalBreakpoint.error) 1958 { 1959 conditionalBreakpoint.hit = false; 1960 breakMessage = "COND ERR"; 1961 } 1962 else 1963 { 1964 conditionalBreakpoint.hit = true; 1965 if(countHit!==false) conditionalBreakpoint.hits++; 1966 breakMessage = ""; 1967 } 1968 1969 // Stop whichever live owner was executing. The CPU condition trap is at 1970 // a clean boundary, so the matching boundary's opcode remains unexecuted. 1971 stopBoundaryAction(); 1972 stopFixedRun(); 1973 if(systemRunning()) pauseSystem(); 1974 1975 if(state && state.pc!==undefined) 1976 { 1977 currentPC = state.pc & 0xffff; 1978 previousObservedPC = currentPC; 1979 } 1980 syncRunIcon(); 1981 syncBreakpointControls(); 1982 1983 window.setTimeout(function() 1984 { 1985 var cpu = liveCPU(); 1986 if(cpu && dbg.isReady()) dbg.cycle({cpu:cpu,force:true}); 1987 },0); 1988 return true; 1989 } 1990 1991 function breakpointActionContext(state,hit) 1992 { 1993 state = state || {}; 1994 return { 1995 A:Number(state.a || 0) & 0xff 1996 ,X:Number(state.x || 0) & 0xff 1997 ,Y:Number(state.y || 0) & 0xff 1998 ,SP:Number(state.sp || 0) & 0xff 1999 ,P:Number(state.p || 0) & 0xff 2000 ,PC:Number(state.pc || 0) & 0xffff 2001 ,INS:Number(state.ic) || 0 2002 ,condition:conditionalBreakpoint.condition 2003 ,hit:hit 2004 }; 2005 } 2006 2007 function conditionalBreakpointMatched(state) 2008 { 2009 if(conditionalBreakpoint.error || typeof(breakpointActionHandler)!=="function") 2010 return conditionalBreakpointHit(state,true); 2011 2012 // A callback must never recursively enter itself through another CPU 2013 // boundary. Fail safe to the ordinary breakpoint stop at the current PC. 2014 if(breakpointActionDispatching) 2015 { 2016 breakpointActionLastError = "Re-entrant breakpoint action dispatch"; 2017 breakpointActionHandler = null; 2018 return conditionalBreakpointHit(state,false); 2019 } 2020 2021 var hit = conditionalBreakpoint.hits + 1; 2022 conditionalBreakpoint.hits = hit; 2023 breakpointActionDispatching = true; 2024 var result = null; 2025 var failed = false; 2026 try 2027 { 2028 result = breakpointActionHandler(breakpointActionContext(state,hit)); 2029 if(result && typeof(result.then)==="function") 2030 throw new Error("Async breakpoint actions are not supported."); 2031 } 2032 catch(err) 2033 { 2034 failed = true; 2035 breakpointActionLastError = err && err.message ? err.message : String(err); 2036 } 2037 finally 2038 { 2039 breakpointActionDispatching = false;
2040 } 2041 2042 if(failed || !breakpointActionHandler || (result && result.halt===true)) 2043 { 2044 breakpointActionHandler = null; 2045 return conditionalBreakpointHit(state,false); 2046 } 2047 2048 // Successful RUN-mode dispatch: keep the observer armed and do not stop 2049 // any execution owner. Returning false lets the matching opcode execute. 2050 conditionalBreakpoint.hit = false; 2051 conditionalBreakpoint.armed = true; 2052 breakMessage = ""; 2053 syncBreakpointControls(); 2054 return false; 2055 } 2056 2057 function evaluateConditionalBreakpoint(state) 2058 { 2059 conditionalBreakpoint.checks++; 2060 var matched = true; 2061 2062 try 2063 { 2064 matched = !!conditionValue(breakConditionAst,state); 2065 conditionalBreakpoint.lastResult = matched; 2066 conditionalBreakpoint.error = null; 2067 } 2068 catch(err) 2069 { 2070 conditionalBreakpoint.lastResult = null; 2071 conditionalBreakpoint.error = err && err.message ? err.message : String(err); 2072 breakMessage = "COND ERR"; 2073 matched = true; // stop visibly rather than silently ignoring an unreadable condition 2074 } 2075 2076 if(!matched) conditionalBreakpoint.skips++; 2077 return matched; 2078 } 2079 2080 function conditionalBreakpointTrap(state) 2081 { 2082 if(!evaluateConditionalBreakpoint(state)) return false; 2083 return conditionalBreakpointMatched(state); 2084 } 2085 2086 function removeConditionalBreakpointObserver() 2087 { 2088 var cpu = liveCPU(); 2089 if(!cpu || !conditionalBreakpoint.armed 2090 || typeof(cpu.clearExecutionCondition)!="function") return false; 2091 return cpu.clearExecutionCondition(conditionalBreakpointTrap); 2092 } 2093 2094 function armConditionalBreakpoint(conditionText) 2095 { 2096 var cpu = liveCPU(); 2097 if(!cpu) 2098 { 2099 breakMessage = "BP unavailable"; 2100 if(currentPC!==null) renderListing(currentPC,true); 2101 return false; 2102 } 2103 2104 var compiled; 2105 try 2106 { 2107 compiled = compileBreakpointCondition(conditionText===undefined ? breakConditionText : conditionText); 2108 if(!compiled.text) throw new Error("Enter a breakpoint condition"); 2109 } 2110 catch(err) 2111 { 2112 breakConditionText = String(conditionText===undefined ? breakConditionText : conditionText).trim(); 2113 breakConditionError = err && err.message ? err.message : String(err); 2114 breakMessage = "BAD COND"; 2115 conditionalBreakpoint.error = breakConditionError; 2116 syncBreakpointControls(); 2117 if(currentPC!==null) renderListing(currentPC,true); 2118 return false; 2119 } 2120 2121 var pcGate = breakpointPcGate(compiled.ast); 2122 var mode = pcGate!==null ? "address" : "condition"; 2123 2124 if(typeof(cpu.setExecutionCondition)!="function") 2125 { 2126 breakMessage = "BP unavailable"; 2127 conditionalBreakpoint.error = "CPU does not provide setExecutionCondition()"; 2128 syncBreakpointControls(); 2129 if(currentPC!==null) renderListing(currentPC,true); 2130 return false; 2131 } 2132 2133 if(conditionalBreakpoint.armed) removeConditionalBreakpointObserver(); 2134 2135 breakConditionText = compiled.text; 2136 breakConditionError = ""; 2137 breakConditionAst = compiled.ast; 2138 conditionalBreakpoint.armed = true; 2139 conditionalBreakpoint.hit = false; 2140 conditionalBreakpoint.condition = compiled.text; 2141 conditionalBreakpoint.mode = mode; 2142 conditionalBreakpoint.address = mode==="address" ? pcGate : null; 2143 conditionalBreakpoint.lastResult = null; 2144 conditionalBreakpoint.error = null; 2145 breakMessage = ""; 2146 2147 var installed = cpu.setExecutionCondition(conditionalBreakpointTrap,pcGate)===true; 2148 2149 if(!installed) 2150 { 2151 conditionalBreakpoint.armed = false; 2152 conditionalBreakpoint.error = "CPU rejected breakpoint observer"; 2153 breakMessage = "BP unavailable"; 2154 syncBreakpointControls(); 2155 if(currentPC!==null) renderListing(currentPC,true); 2156 return false;
2157 } 2158 2159 syncBreakpointControls(); 2160 if(currentPC!==null) renderListing(currentPC,true); 2161 return compiled.text; 2162 } 2163 2164 function clearConditionalBreakpoint() 2165 { 2166 removeConditionalBreakpointObserver(); 2167 2168 // Disarm only. The editor text, parsed AST and validation state are kept 2169 // intact so the exact same condition can be armed again with one click. 2170 conditionalBreakpoint.armed = false; 2171 conditionalBreakpoint.hit = false; 2172 conditionalBreakpoint.condition = ""; 2173 conditionalBreakpoint.mode = ""; 2174 conditionalBreakpoint.address = null; 2175 conditionalBreakpoint.lastResult = null; 2176 conditionalBreakpoint.error = null; 2177 breakMessage = ""; 2178 2179 syncBreakpointControls(); 2180 if(currentPC!==null) updateNavigationStatus(currentPC); 2181 return true; 2182 } 2183 2184 this.html = function(body_id,wrapper_id) 2185 { 2186 this.body_id = body_id; 2187 oCOM.POPUP.set_state(wrapper_id,true); 2188 return "<div class=appbox style='text-align:left;height:auto;min-height:350px;width:350px;padding:0 0 0 1px;margin:0'>" 2189 2190 +"<div class=marginless style='border:0;font-family:Arial,sans-serif;font-size:9px;line-height:18px'>" 2191 +"<div style='display:flex;align-items:center;gap:3px;white-space:nowrap;min-width:0'>" 2192 +"<span style='font-size:12px;'>STEP TRACE </span>" 2193 //+"<span style='font-size:15px;font-weight:400;line-height:22px;margin-right:2px'>STEP TRACE</span>" 2194 +"<i id=cpuDbg_play class='fa fa-pause-circle' style='font-size:11px;cursor:pointer' title='pause CPU execution' onclick='oEMU.component.CPU.Apple2Debug.toggleRun(this)'></i>" 2195 +"<i class='fa fa-sign-in-alt' style='font-size:11px;cursor:pointer' title='step one live instruction (F11)' onclick='oEMU.component.CPU.Apple2Debug.step()'></i>" 2196 +"<i class='fa fa-paw' style='font-size:11px;cursor:pointer' title='step over JSR/BRK (F10)' onclick='oEMU.component.CPU.Apple2Debug.stepOver()'></i>" 2197 +"<i class='fa fa-sign-out-alt' style='font-size:11px;cursor:pointer' title='step out of current routine (Shift+F11)' onclick='oEMU.component.CPU.Apple2Debug.stepOut()'></i>" 2198 2199 +"<div style='width:22px'></div>" 2200 2201 +"<div class='appbut skinny'><i id='cpuDbg_bootTrigger' class='fa fa-coffee' style='opacity:.35;font-size:10px' title='bootlog trigger disabled' onclick='oEMU.component.CPU.Apple2Debug.toggleBootLogTrigger(this)'></i></div>" 2202 +"<input id='cpuDbg_bootStart' type='text' value='' maxlength='6' spellcheck='false' placeholder='$....' title='Bootlog start address; blank starts immediately' style='width:43px;height:18px;padding:0 2px;box-sizing:border-box;font-family:"+listingFontFamily+";font-size:9px;text-transform:uppercase' onchange='oEMU.component.CPU.Apple2Debug.setBootLogAddresses()'>" 2203 //+"<span title='bootlog start → stop'>›</span>" 2204 +"<i class='fa fa-play'></i>" 2205 +"<input id='cpuDbg_bootStop' type='text' value='' maxlength='6' spellcheck='false' placeholder='$....' title='Bootlog stop address; blank stops when the buffer is full' style='width:43px;height:18px;padding:0 2px;box-sizing:border-box;font-family:"+listingFontFamily+";font-size:9px;text-transform:uppercase' onchange='oEMU.component.CPU.Apple2Debug.setBootLogAddresses()'>" 2206 +"<div class='appbut skinny' onclick='oEMU.component.CPU.Apple2Debug.downloadBootLog()'><i class='fa fa-cloud-download-alt' style='font-size:10px' title='download bootlog'></i></div>" 2207 +"<span style='flex:1 1 auto'></span>" 2208 +"<div class='appbut' onclick=\"oEMU.component.CPU.Apple2Debug.close();oCOM.POPUP.toggle('"+wrapper_id+"');\" style='text-align:center;margin-left:0;padding:4px 6px;font-size:11px'>x</div>" 2209 +"</div>" 2210 +"<div style='display:flex;align-items:center;gap:3px;white-space:nowrap'>" 2211 2212 +"NAV " 2213 2214 +"<button type='button' aria-label='Previous instruction; hold for page up' title='Previous instruction; hold 1/2s for page up' " 2215 +"onmousedown='return oEMU.component.CPU.Apple2Debug.navButtonDown(this,-1)' onmouseup='return oEMU.component.CPU.Apple2Debug.navButtonUp(this)' onmouseleave='return oEMU.component.CPU.Apple2Debug.navButtonCancel(this)' " 2216 +"ontouchstart='return oEMU.component.CPU.Apple2Debug.navButtonDown(this,-1)' ontouchend='return oEMU.component.CPU.Apple2Debug.navButtonUp(this)' ontouchcancel='return oEMU.component.CPU.Apple2Debug.navButtonCancel(this)' " 2217 +"style='border:0;background:transparent;-webkit-appearance:none;appearance:none;padding:0 1px;margin:0;line-height:1;font-size:11px;cursor:pointer'>↑</button>" 2218 +"<button type='button' aria-label='Next instruction; hold for page down' title='Next instruction; hold 1/2s for page down' " 2219 +"onmousedown='return oEMU.component.CPU.Apple2Debug.navButtonDown(this,1)' onmouseup='return oEMU.component.CPU.Apple2Debug.navButtonUp(this)' onmouseleave='return oEMU.component.CPU.Apple2Debug.navButtonCancel(this)' " 2220 +"ontouchstart='return oEMU.component.CPU.Apple2Debug.navButtonDown(this,1)' ontouchend='return oEMU.component.CPU.Apple2Debug.navButtonUp(this)' ontouchcancel='return oEMU.component.CPU.Apple2Debug.navButtonCancel(this)' " 2221 +"style='border:0;background:transparent;-webkit-appearance:none;appearance:none;padding:0 1px;margin:0;line-height:1;font-size:11px;cursor:pointer'>↓</button>" 2222 // 27 fixed-width characters are required for 2223 // "PC $FFFF INS $FFFFFFFFFFFF". 146px clipped the 2224 // least-significant INS hex digit, making the visible 2225 // counter appear to advance only once per 16 opcodes. 2226 +"<input id='cpuDbg_navStatus' type='text' readonly value='PC $0000 INS $000000000000' aria-label='Live PC and instruction counter' title='Live PC and instruction counter; read-only and copyable' style='width:158px;height:16px;padding:0 2px;box-sizing:border-box;border:1px solid #bbb;background:#fff;font-family:"+listingFontFamily+";font-size:9px;
2226cursor:text'>" 2227 +"<i id='cpuDbg_trackPc' class='fa fa-lock' role='button' aria-pressed='true' title='Track PC enabled — click to unlock the listing' onclick='oEMU.component.CPU.Apple2Debug.toggleTrackPC()' style='font-size:10px;cursor:pointer;margin-left:4px'></i>" 2228 +"<i id='cpuDbg_showLoopSteps' class='fa fa-retweet' role='button' aria-pressed='false' title='Closed-loop step skipper disabled — click to accelerate proven closed loops' onclick='oEMU.component.CPU.Apple2Debug.toggleClosedLoopSkip()' style='opacity:.32;font-size:10px;cursor:pointer;margin-left:3px'></i>" 2229 +"<select id='cpuDbg_speed' title='STEP TRACE execution speed' onchange='oEMU.component.CPU.Apple2Debug.setRunSpeed(this.value)' style='width:90px;height:18px;padding:0;font-size:9px'>" 2230 +"<option value='1'>1 IPS</option>" 2231 +"<option value='10'>10 IPS</option>" 2232 +"<option value='100'>100 IPS</option>" 2233 +"<option value='1000'>1000 IPS</option>" 2234 +"<option value='system' selected>Max (SYSTEM)</option>" 2235 +"</select>" 2236 +"</div>" 2237 +"<div style='display:flex;align-items:center;gap:3px;white-space:nowrap'>" 2238 +"<span>BREAK IF</span>" 2239 +"<input id='cpuDbg_breakCond' type='text' value='' spellcheck='false' title='"+conditionHelp+"' placeholder='e.g. PC==$C600 && A==$10' style='flex:1 1 auto;min-width:40px;height:18px;padding:0 3px;box-sizing:border-box;font-family:"+listingFontFamily+";font-size:9px' oninput='oEMU.component.CPU.Apple2Debug.setBreakpointCondition(this.value)'>" 2240 +"<span id='cpuDbg_breakStatus' aria-live='polite' style='display:none;flex:0 0 auto;white-space:nowrap;font-family:"+listingFontFamily+";font-size:9px'></span>" 2241 +"<button id='cpuDbg_breakArm' type='button' title='Arm conditional breakpoint (F9)' onclick='oEMU.component.CPU.Apple2Debug.toggleConditionalBreakpointFromInput()' style='font-size:9px;padding:0 4px'>Arm</button>" 2242 +"</div>" 2243 +"<div style='white-space:nowrap'>LISTING Columns <input id='cpuDbg_columns' type='text' value='"+listingColumns+"' spellcheck='false' style='width:220px;font-family:"+listingFontFamily+";font-size:9px' onchange='oEMU.component.CPU.Apple2Debug.setListingColumns(this.value)'></div>" 2244 +"<div style='white-space:nowrap;font-size:9px'>" 2245 +"<button type='button' onclick=\"oEMU.component.CPU.Apple2Debug.applyListingPreset('default')\" style='font-size:9px;padding:0 3px'>default ▦</button> " 2246 +"<button type='button' onclick=\"oEMU.component.CPU.Apple2Debug.applyListingPreset('wide')\" style='font-size:9px;padding:0 3px'>wide ▦</button> " 2247 +"<button type='button' onclick=\"oEMU.component.CPU.Apple2Debug.applyListingPreset('compact')\" style='font-size:9px;padding:0 3px'>compact ▦</button>" 2248 +" SYMBOLS " 2249 +"<button type='button' title='Load RetroAppleJS assembler symbol export (.symbols.json) or simple text symbol map' onclick=\"oEMU.component.CPU.Apple2Debug.chooseSymbolFile()\" style='font-size:9px;padding:0 3px'>load</button> " 2250 +"<button id='cpuDbg_symbolClear' type='button' title='Clear loaded symbol table' onclick=\"oEMU.component.CPU.Apple2Debug.clearSymbols()\" style='font-size:9px;padding:0 3px'>clear</button> " 2251 +"<span id='cpuDbg_symbolStatus' title='No external symbol table loaded'>none</span>" 2252 +"<input id='cpuDbg_symbolFile' type='file' accept='.symbols.json,.json,.sym,.txt,application/json,text/plain' style='display:none' onchange='if(this.files&&this.files[0]) oEMU.component.CPU.Apple2Debug.loadSymbolFile(this.files[0]);this.value=\"\";'>" 2253 +"</div>" 2254 +"</div>" 2255 +"<div id='"+body_id+"' class=marginless style='width:348px;
2255height:"+(listingRows*rowPixelHeight)+"px;border:0;font-family:"+listingFontFamily+";font-size:"+listingFontSize+"px;font-weight:500;font-kerning:none;font-variant-ligatures:none;color:#000;white-space:nowrap;overflow-x:auto;overflow-y:hidden;touch-action:none;'></div>" 2256 +"<div id='"+body_id+"_regs' class='DBG_traceRegisterInfo' style='width:348px;height:18px;line-height:18px;border:0;padding:1px 0 0 2px;font-family:"+listingFontFamily+";font-size:10px;font-weight:500;white-space:nowrap'>A=00 X=00 Y=00 SP=FF SR=<sub>n</sub>0<sub>v</sub>0<sub>-</sub>1<sub>b</sub>0<sub>d</sub>0<sub>i</sub>1<sub>z</sub>0<sub>c</sub>0</div>" 2257 +"</div></div>"; 2258 }; 2259 2260 this.chooseSymbolFile = function() 2261 { 2262 var input = document.getElementById("cpuDbg_symbolFile"); 2263 if(input) input.click(); 2264 }; 2265 2266 this.loadSymbolsText = function(text,fileName) 2267 { 2268 text = String(text==null ? "" : text); 2269 var trimmed = text.trim(); 2270 try 2271 { 2272 if(trimmed.charAt(0)==="{" || trimmed.charAt(0)==="[") 2273 return loadSymbolObject(JSON.parse(trimmed),fileName); 2274 return loadSymbolTextMap(text,fileName); 2275 } 2276 catch(err) 2277 { 2278 symbolState.error = err && err.message ? err.message : String(err); 2279 syncSymbolControls(); 2280 throw err; 2281 } 2282 }; 2283 2284 this.loadSymbolFile = function(file) 2285 { 2286 if(!file) return false; 2287 var reader = new FileReader(); 2288 reader.onload = function() 2289 { 2290 try 2291 { 2292 dbg.loadSymbolsText(reader.result,file.name || "symbols"); 2293 } 2294 catch(err) 2295 { 2296 alert("Cannot load symbol table: "+(err && err.message ? err.message : err)); 2297 } 2298 }; 2299 reader.onerror = function() 2300 { 2301 symbolState.error = "Cannot read symbol file"; 2302 syncSymbolControls(); 2303 alert(symbolState.error); 2304 }; 2305 reader.readAsText(file); 2306 return true; 2307 }; 2308 2309 this.resolveSymbol = function(name) 2310 { 2311 var key = String(name==null ? "" : name).trim().toUpperCase(); 2312 var record = key ? loadedSymbolNames[key] : null; 2313 return record ? (record.value & 0xffff) : null; 2314 }; 2315 2316 this.symbol = function(name) 2317 { 2318 var key = String(name==null ? "" : name).trim().toUpperCase(); 2319 var record = key ? loadedSymbolNames[key] : null; 2320 return record ? {name:record.name,value:record.value,type:record.type} : null; 2321 }; 2322 2323 this.symbols = function() 2324 { 2325 return Object.keys(loadedSymbolNames).sort().map(function(key) 2326 { 2327 var record = loadedSymbolNames[key]; 2328 return {name:record.name,value:record.value,type:record.type}; 2329 }); 2330 }; 2331 2332 this.clearSymbols = function() 2333 { 2334 return resetLoadedSymbols(); 2335 }; 2336 2337 this.downloadBootLog = function() 2338 { 2339 var cpu = liveCPU(); 2340 if(!cpu || typeof(cpu.getBootLogBase64)!="function") 2341 { 2342 alert("Bootlog download is unavailable: getBootLogBase64() is missing."); 2343 return; 2344 } 2345 2346 var base64 = cpu.getBootLogBase64(); 2347 var filename = "apple2_bootlog_" + new Date().toISOString().replace(/[:.]/g,"-") + ".txt"; 2348 var blob = new Blob([base64],{type:"text/plain;charset=utf-8"}); 2349 var url = URL.createObjectURL(blob); 2350 var a = document.createElement("a"); 2351 a.href = url; 2352 a.download = filename; 2353 a.style.display = "none"; 2354 document.body.appendChild(a); 2355 a.click(); 2356 document.body.removeChild(a); 2357 URL.revokeObjectURL(url); 2358 }; 2359 2360 function bootAddressInput(id,label,example) 2361 { 2362 var input = document.getElementById(id); 2363 var text = input ? String(input.value || "").trim() : ""; 2364 if(text==="") return {ok:true,value:null}; 2365 2366 var addr = parseAddress(text); 2367 if(addr!==null) return {ok:true,value:addr}; 2368 2369 alert("Invalid "+label+" address. Use for example "+example+", or leave blank."); 2370 if(input) 2371 { 2372 input.focus(); 2373 if(typeof(input.select)==="function") input.select(); 2374 } 2375 return {ok:false,value:null}; 2376 } 2377 2378 function bootRangeFromInputs() 2379 { 2380 var start = bootAddressInput("cpuDbg_bootStart","start","$6000"); 2381 if(!start.ok) return null; 2382 var stop = bootAddressInput("cpuDbg_bootStop","stop","$FF69"); 2383 if(!stop.ok) return null; 2384 return {start:start.value,stop:stop.value}; 2385 } 2386 2387 this.setBootLogAddresses = function() 2388 { 2389 var cpu = liveCPU(); 2390 if(!cpu || typeof(cpu.BOOTparam)!="function" || typeof(cpu.setBootLogTrigger)!="function") return false;
2391 var range = bootRangeFromInputs(); 2392 if(!range) return false; 2393 var state = cpu.BOOTparam(); 2394 state = cpu.setBootLogTrigger(range.start,range.stop,!!state.bDebug_boot); 2395 updateBootTriggerIcon(document.getElementById("cpuDbg_bootTrigger"),state); 2396 return true; 2397 }; 2398 2399 this.toggleBootLogTrigger = function(el) 2400 { 2401 var cpu = liveCPU(); 2402 if(!cpu || typeof(cpu.BOOTparam)!="function" || typeof(cpu.setBootLogTrigger)!="function") 2403 { 2404 alert("Bootlog trigger is unavailable in this CPU build."); 2405 return; 2406 } 2407 2408 var state = cpu.BOOTparam(); 2409 if(state.bDebug_boot) 2410 { 2411 state = cpu.setBootLogTrigger(state.triggerAddress,state.stopAddress,false); 2412 updateBootTriggerIcon(el,state); 2413 return; 2414 } 2415 2416 var range = bootRangeFromInputs(); 2417 if(!range) return; 2418 state = cpu.setBootLogTrigger(range.start,range.stop,true); 2419 updateBootTriggerIcon(el,state); 2420 }; 2421 2422 /* 2423 * Short press on NAV walks one instruction. Holding the same arrow for 2424 * 500 ms performs one page move, matching the restart button's 2425 * short-press/hold interaction model in tab 1.2. 2426 */ 2427 this.navButtonDown = function(el,direction) 2428 { 2429 if(!el || el._cpuDbgNavArmed) return false; 2430 2431 el._cpuDbgNavArmed = true; 2432 el._cpuDbgNavLong = false; 2433 el._cpuDbgNavDirection = Number(direction)<0 ? -1 : 1; 2434 el._cpuDbgNavTimer = window.setTimeout(function() 2435 { 2436 el._cpuDbgNavTimer = null; 2437 if(!el._cpuDbgNavArmed) return; 2438 el._cpuDbgNavLong = true; 2439 dbg.navigatePage(el._cpuDbgNavDirection); 2440 },500); 2441 return false; 2442 }; 2443 2444 this.navButtonUp = function(el) 2445 { 2446 if(!el || !el._cpuDbgNavArmed) return false; 2447 2448 if(el._cpuDbgNavTimer!=null) 2449 { 2450 window.clearTimeout(el._cpuDbgNavTimer); 2451 el._cpuDbgNavTimer = null; 2452 } 2453 2454 var longPress = !!el._cpuDbgNavLong; 2455 var direction = el._cpuDbgNavDirection<0 ? -1 : 1; 2456 el._cpuDbgNavArmed = false; 2457 el._cpuDbgNavLong = false; 2458 2459 if(!longPress) dbg.navigateRows(direction); 2460 return false; 2461 }; 2462 2463 this.navButtonCancel = function(el) 2464 { 2465 if(!el) return false; 2466 if(el._cpuDbgNavTimer!=null) window.clearTimeout(el._cpuDbgNavTimer); 2467 el._cpuDbgNavTimer = null; 2468 el._cpuDbgNavArmed = false; 2469 el._cpuDbgNavLong = false; 2470 return false; 2471 }; 2472 2473 2474 this.isReady = function() 2475 { 2476 return typeof(document)!="undefined" && !!document.getElementById(this.body_id); 2477 }; 2478 2479 this.open = function() 2480 { 2481 var cpu = liveCPU(); 2482 if(!cpu || !this.isReady()) return false; 2483 if(!ensureInit({scrollH:listingRows})) return false; 2484 2485 bindNavigation(); 2486 var sel = document.getElementById("cpuDbg_speed"); 2487 if(sel) sel.value = runMode; 2488 var input = document.getElementById("cpuDbg_columns"); 2489 if(input) input.value = listingColumns; 2490 var cond = document.getElementById("cpuDbg_breakCond"); 2491 if(cond) cond.value = breakConditionText; 2492 syncSymbolControls(); 2493 2494 currentPC = cpu.watch().pc & 0xffff; 2495 previousObservedPC = currentPC; 2496 renderListing(currentPC,true); 2497 syncRunIcon(); 2498 2499 if(typeof(cpu.BOOTparam)=="function") 2500 updateBootTriggerIcon(document.getElementById("cpuDbg_bootTrigger"),cpu.BOOTparam()); 2501 return true; 2502 }; 2503 2504 this.cycle = function(obj) 2505 { 2506 var cpu = obj && obj.cpu && typeof(obj.cpu.watch)=="function" ? obj.cpu : liveCPU(); 2507 if(!cpu || !this.isReady()) return false; 2508 if(!ensureInit({scrollH:listingRows})) return false; 2509 2510 if(typeof(cpu.BOOTparam)=="function") 2511 updateBootTriggerIcon(document.getElementById("cpuDbg_bootTrigger"),cpu.BOOTparam()); 2512 2513 var watch = cpu.watch(); 2514 updateRegisterStatus(watch,!!(obj && obj.force)); 2515 var pc = watch.pc & 0xffff; 2516 if(previousObservedPC!==null && previousObservedPC!==pc) 2517 rememberSequential(previousObservedPC,pc); 2518 2519 var changed = currentPC!==pc; 2520 previousObservedPC = pc; 2521 currentPC = pc; 2522 2523 if(followPC) 2524 { 2525 if(changed || (obj && obj.force)) 2526 renderListing(pc,!!(obj && obj.force)); 2527 else 2528 // PC-stable code (for example JMP $xxxx to itself) still 2529 // completes instructions. Keep NAV/INS live without repainting 2530 // the listing or changing the displayed PC. 2531 updateNavigationStatus(pc); 2532 } 2533 else 2534 renderListing(pc,!!(obj && obj.force)); 2535 2536 syncRunIcon(); 2537 return true; 2538 }; 2539 2540 this.setFollowPC = function(enabled) 2541 {
2542 return setFollowState(enabled,true); 2543 }; 2544 2545 this.toggleTrackPC = function() 2546 { 2547 return this.setFollowPC(!followPC); 2548 }; 2549 2550 this.setClosedLoopSkip = function(enabled) 2551 { 2552 showLoopSteps = !enabled; 2553 resetClosedLoopDisplayState(); 2554 syncLoopControl(); 2555 if(currentPC!==null) renderListing(currentPC,true); 2556 return !showLoopSteps; 2557 }; 2558 2559 this.toggleClosedLoopSkip = function() 2560 { 2561 return this.setClosedLoopSkip(showLoopSteps); 2562 }; 2563 2564 // Compatibility aliases retained for callers that used the older 2565 // display-oriented API names. 2566 this.setShowLoopSteps = function(enabled) 2567 { 2568 this.setClosedLoopSkip(!enabled); 2569 return showLoopSteps; 2570 }; 2571 2572 this.toggleLoopSteps = function() 2573 { 2574 this.toggleClosedLoopSkip(); 2575 return showLoopSteps; 2576 }; 2577 2578 this.navigateRows = function(delta) 2579 { 2580 delta = Number(delta)|0; 2581 if(delta===0) return 0; 2582 var cpu = liveCPU(); 2583 if(!cpu || !ensureInit({scrollH:listingRows})) return 0; 2584 2585 var pc = cpu.watch().pc & 0xffff; 2586 currentPC = pc; 2587 if(followPC) setFollowState(false,false); 2588 if(manualTop===null) manualTop = lastViewTop===null ? pc : lastViewTop; 2589 2590 var adr = manualTop; 2591 var wanted = Math.abs(delta); 2592 var dir = delta<0 ? -1 : 1; 2593 var moved = 0; 2594 2595 for(var i=0;i<wanted;i++) 2596 { 2597 if(dir>0) 2598 { 2599 var d = decodeAt(adr); 2600 if(!d) break; 2601 previousBoundary[d.next] = adr; 2602 adr = d.next; 2603 } 2604 else 2605 { 2606 var prev = predecessorOf(adr); 2607 if(prev<0) break; 2608 adr = prev; 2609 } 2610 moved++; 2611 } 2612 2613 if(moved>0) manualTop = adr; 2614 renderListing(pc,true); 2615 return moved*dir; 2616 }; 2617 2618 this.navigatePage = function(direction) 2619 { 2620 direction = Number(direction)<0 ? -1 : 1; 2621 return this.navigateRows(direction*Math.max(1,listingRows-1)); 2622 }; 2623 2624 this.setRunSpeed = function(value) 2625 { 2626 value = String(value || "system").toLowerCase(); 2627 if(["1","10","100","1000","system"].indexOf(value)<0) value = "system"; 2628 2629 var wasRunning = executionRunning(); 2630 if(runMode==="system" && systemRunning()) pauseSystem(); 2631 stopBoundaryAction(); 2632 stopFixedRun(); 2633 runMode = value; 2634 if(runMode==="system") resetClosedLoopDisplayState(); 2635 2636 var sel = document.getElementById("cpuDbg_speed"); 2637 if(sel && sel.value!==runMode) sel.value = runMode; 2638 2639 if(wasRunning) startExecution(); 2640 else syncRunIcon(); 2641 return runMode; 2642 }; 2643 2644 this.toggleRun = function(el) 2645 { 2646 if(executionRunning()) pauseExecution(); 2647 else startExecution(); 2648 syncRunIcon(el); 2649 this.cycle({cpu:liveCPU(),force:true}); 2650 }; 2651 2652 this.play = function(bPlay) 2653 { 2654 if(!!bPlay !== executionRunning()) this.toggleRun(document.getElementById("cpuDbg_play")); 2655 }; 2656 2657 this.step = function() 2658 { 2659 manualStepPause = true; 2660 // A direct Step-In request is always exactly one instruction. The 2661 // closed-loop skipper applies only to debugger-owned continuous run paths. 2662 resetClosedLoopDisplayState(); 2663 var machine = liveMachine(); 2664 if(!machine || typeof(machine.stepLiveInstruction)!="function") return false; 2665 2666 pauseExecution(); 2667 var result = machine.stepLiveInstruction(); 2668 if(result && result.ticks>0) rememberSequential(result.startPC,result.endPC); 2669 this.cycle({cpu:machine.cpuObj(),force:true}); 2670 return result; 2671 }; 2672 2673 this.stepOver = function() 2674 { 2675 var cpu = liveCPU(); 2676 if(!cpu || !ensureInit({scrollH:listingRows})) return false; 2677 var state = cpu.watch(); 2678 var pc = state.pc & 0xffff; 2679 var d = decodeAt(pc); 2680 if(!d) return false; 2681 2682 // Only call-like operations need a temporary run. Everything else is 2683 // exactly one live instruction. 2684 if(d.b0!==0x20 && d.b0!==0x00) return this.step(); 2685 2686 manualStepPause = true; 2687 return startBoundaryAction({ 2688 "type":"over" 2689 ,"returnPC":d.next 2690 ,"startSP":state.sp & 0xff 2691 ,"instructions":0 2692 }); 2693 }; 2694 2695 this.stepOut = function() 2696 { 2697 var cpu = liveCPU(); 2698 if(!cpu || !ensureInit({scrollH:listingRows})) return false;
2699 manualStepPause = true; 2700 return startBoundaryAction({ 2701 "type":"out" 2702 ,"jsrDepth":0 2703 ,"irqDepth":0 2704 ,"instructions":0 2705 }); 2706 }; 2707 2708 this.setBreakpointActionHandler = function(fn) 2709 { 2710 if(fn!==null && typeof(fn)!=="function") 2711 throw new TypeError("Breakpoint action handler must be a function or null."); 2712 breakpointActionHandler = fn; 2713 breakpointActionLastError = null; 2714 return !!breakpointActionHandler; 2715 }; 2716 2717 this.breakpointActionState = function() 2718 { 2719 return { 2720 active:typeof(breakpointActionHandler)==="function" 2721 ,dispatching:breakpointActionDispatching 2722 ,lastError:breakpointActionLastError 2723 }; 2724 }; 2725 2726 this.setBreakpointCondition = function(value) 2727 { 2728 var nextText = String(value==null ? "" : value).trim(); 2729 2730 // Editing an armed expression must never leave a hidden old predicate 2731 // running. Disarm it immediately; the next Arm click installs exactly 2732 // what is visible in the editor. 2733 if(conditionalBreakpoint.armed && nextText!==conditionalBreakpoint.condition) 2734 clearConditionalBreakpoint(); 2735 2736 breakConditionText = nextText; 2737 try 2738 { 2739 var compiled = breakConditionText 2740 ? compileBreakpointCondition(breakConditionText) 2741 : {text:"",ast:null}; 2742 breakConditionAst = compiled.ast; 2743 breakConditionError = ""; 2744 conditionalBreakpoint.error = null; 2745 if(!conditionalBreakpoint.armed) conditionalBreakpoint.hit = false; 2746 breakMessage = ""; 2747 } 2748 catch(err) 2749 { 2750 breakConditionAst = null; 2751 breakConditionError = err && err.message ? err.message : String(err); 2752 breakMessage = "BAD COND"; 2753 } 2754 syncBreakpointControls(); 2755 if(currentPC!==null) updateNavigationStatus(currentPC); 2756 return breakConditionError ? false : breakConditionText; 2757 }; 2758 2759 this.armConditionalBreakpoint = function(condition) 2760 { 2761 return armConditionalBreakpoint(condition); 2762 }; 2763 2764 this.armConditionalBreakpointFromInput = function() 2765 { 2766 var cond = document.getElementById("cpuDbg_breakCond"); 2767 return armConditionalBreakpoint(cond ? cond.value : breakConditionText); 2768 }; 2769 2770 this.toggleConditionalBreakpointFromInput = function() 2771 { 2772 if(conditionalBreakpoint.armed) return clearConditionalBreakpoint(); 2773 return this.armConditionalBreakpointFromInput(); 2774 }; 2775 2776 // Retained as a compatibility/API alias for callers that explicitly want 2777 // to disarm. The visible UI intentionally has no separate Clear button. 2778 this.clearConditionalBreakpoint = function() 2779 { 2780 return clearConditionalBreakpoint(); 2781 }; 2782 2783 this.setListingColumns = function(value) 2784 { 2785 if(String(value || "").trim()) listingColumns = String(value).trim(); 2786 var input = document.getElementById("cpuDbg_columns"); 2787 if(input && input.value!==listingColumns) input.value = listingColumns; 2788 if(currentPC!==null) renderListing(currentPC,true); 2789 return listingColumns; 2790 }; 2791 2792 this.applyListingPreset = function(name) 2793 { 2794 var preset = listingColumnPresets[name]; 2795 if(!preset) return false; 2796 this.setListingColumns(preset); 2797 return true; 2798 }; 2799 2800 this.close = function() 2801 { 2802 // Never leave an invisible debugger-owned run or execution trap active. 2803 stopBoundaryAction(); 2804 stopFixedRun(); 2805 resetClosedLoopDisplayState(); 2806 breakpointActionHandler = null; 2807 breakpointActionDispatching = false; 2808 breakpointActionLastError = null; 2809 clearConditionalBreakpoint(); 2810 syncRunIcon(); 2811 }; 2812 2813 this.liveState = function() 2814 { 2815 var cpu = liveCPU(); 2816 return { 2817 "pc":cpu ? (cpu.watch().pc & 0xffff) : null 2818 ,"runMode":runMode 2819 ,"running":executionRunning() 2820 ,"systemRunning":systemRunning() 2821 ,"resumePct":resumePct 2822 ,"followPC":followPC 2823 ,"showLoopSteps":showLoopSteps 2824 ,"skipClosedLoops":!showLoopSteps 2825 ,"closedLoop":activeClosedLoop ? Object.assign({},activeClosedLoop) : null 2826 ,"loopDisplayStats":Object.assign({},loopDisplayStats) 2827 ,"viewTop":lastViewTop 2828 ,"mappedBus":!!(liveHW() && typeof(liveHW().safe_read)=="function") 2829 ,"traceMaskedRange":"$C000-$C0FF" 2830 ,"tracePeripheralROM":"$C100-$CFFF" 2831 ,"pcInPeripheralROM":cpu ? peripheralRomAddress(cpu.watch().pc) : false 2832 ,"liveStepAPI":!!(liveMachine() && typeof(liveMachine().stepLiveInstruction)=="function") 2833 ,"boundaryAction":boundaryAction ? Object.assign({},boundaryAction) : null
2834 ,"conditionalBreakpoint":{ 2835 "armed":conditionalBreakpoint.armed 2836 ,"hit":conditionalBreakpoint.hit 2837 ,"hits":conditionalBreakpoint.hits 2838 ,"checks":conditionalBreakpoint.checks 2839 ,"skips":conditionalBreakpoint.skips 2840 ,"condition":conditionalBreakpoint.condition 2841 ,"mode":conditionalBreakpoint.mode 2842 ,"address":conditionalBreakpoint.address 2843 ,"editorCondition":breakConditionText 2844 ,"lastResult":conditionalBreakpoint.lastResult 2845 ,"error":conditionalBreakpoint.error || breakConditionError || null 2846 } 2847 ,"symbols":Object.assign({},symbolState) 2848 ,"cacheHits":cacheHits 2849 ,"cacheMisses":cacheMisses 2850 ,"domWrites":domWrites 2851 }; 2852 }; 2853}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.