PageSourceSearch

https://retroapplejs.github.io/res/EMU_apple2debug.js

js retroapplejs.github.io collected 2026-10-03 09:50:35 UTC 104,203 bytes, 2,853 lines download raw bytes

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(/&nbsp;/gi," ")
268            .replace(/&lt;/gi,"<")
269            .replace(/&gt;/gi,">")
270            .replace(/&amp;/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&nbsp;</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&nbsp; 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                        +"&nbsp; 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.