From 16003787e41874251d2c7cf3f1e219c0110a25a1 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Wed, 6 Aug 2025 16:00:34 -0700 Subject: [PATCH 01/18] Starting functionality for a better traceback Start of implementing a traceback using the debugging extension --- src/engine/block-utility.js | 5 ++++- src/engine/runtime.js | 7 +++++-- src/extensions/jg_debugging/index.js | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/engine/block-utility.js b/src/engine/block-utility.js index b5bf4e12abf..91b2e4bc308 100644 --- a/src/engine/block-utility.js +++ b/src/engine/block-utility.js @@ -231,7 +231,10 @@ class BlockUtility { // and confuse the calling block when we return to it. const callerThread = this.thread; const callerSequencer = this.sequencer; - const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget); + + const result = this.sequencer.runtime.startHats( + requestedHat, optMatchFields, optTarget, callerThread + ); // Restore thread and sequencer to prior values before we return to the calling block. this.thread = callerThread; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 8076a7154b9..0a62d0c9c5a 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2612,9 +2612,10 @@ class Runtime extends EventEmitter { * @param {!string} requestedHatOpcode Opcode of hats to start. * @param {object=} optMatchFields Optionally, fields to match on the hat. * @param {Target=} optTarget Optionally, a target to restrict to. + * @param {Thread=} optParentThread Optionally, a parent thread. * @return {Array.} List of threads started by this function. */ - startHats (requestedHatOpcode, optMatchFields, optTarget) { + startHats (requestedHatOpcode, optMatchFields, optTarget, optParentThread) { if (!this._hats.hasOwnProperty(requestedHatOpcode)) { // No known hat with this opcode. return; @@ -2695,7 +2696,9 @@ class Runtime extends EventEmitter { thread.goToNextBlock(); } }); - this.emit(Runtime.HATS_STARTED, requestedHatOpcode, optMatchFields, optTarget, newThreads); + this.emit(Runtime.HATS_STARTED, + requestedHatOpcode, optMatchFields, optTarget, newThreads, optParentThread + ); return newThreads; } diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 317e56780bf..1367e47e853 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -161,6 +161,24 @@ class jgDebuggingBlocks { this._logs = []; this.commandSet = {}; this.commandExplanations = {}; + + runtime.on('HATS_STARTED', (opcode, fields, target, threads, caller_thread) => { + if (threads.length === 0) return; + console.debug(opcode, fields, target, threads, caller_thread); + + const starting_stack = !caller_thread ? new Set() : caller_thread.traceback; + + for (let thread of threads) { + const top_block = thread.blockContainer.getBlock(thread.topBlock); + const stack_point = { + target: thread.target, + top_block, + }; + const stack = new Set(starting_stack); + stack.add(stack_point); + thread.traceback = stack; + } + }); } /** @@ -229,6 +247,11 @@ class jgDebuggingBlocks { { opcode: 'breakpoint', blockType: BlockType.COMMAND, + }, + '---', + { + opcode: 'trace', + blockType: BlockType.COMMAND, } ] }; @@ -406,6 +429,8 @@ class jgDebuggingBlocks { this._addLog(text, "color: red;"); } + trace(args, util) {} + breakpoint() { this.runtime.pause(); } From e75feee70c139ee43c5f851283b9588b27de6641 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Wed, 6 Aug 2025 18:36:59 -0700 Subject: [PATCH 02/18] Update JSExecute definition of startHats So it adds the parent thread to match with the block-utility one. --- src/compiler/jsexecute.js | 20 ++++++++++---------- src/engine/runtime.js | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/compiler/jsexecute.js b/src/compiler/jsexecute.js index 77a383804fb..56e5641bb1c 100644 --- a/src/compiler/jsexecute.js +++ b/src/compiler/jsexecute.js @@ -57,7 +57,7 @@ runtimeFunctions.nullish = `const nullish = (check, alt) => { */ runtimeFunctions.startHats = `const startHats = (requestedHat, optMatchFields) => { const thread = globalState.thread; - const threads = thread.target.runtime.startHats(requestedHat, optMatchFields); + const threads = thread.target.runtime.startHats(requestedHat, optMatchFields, null, thread); return threads; }`; @@ -596,7 +596,7 @@ runtimeFunctions.tan = `const tan = (angle) => { return Math.round(Math.tan((Math.PI * angle) / 180) * 1e10) / 1e10; }`; -runtimeFunctions.resolveImageURL = `const resolveImageURL = imgURL => +runtimeFunctions.resolveImageURL = `const resolveImageURL = imgURL => typeof imgURL === 'object' && imgURL.type === 'canvas' ? Promise.resolve(imgURL.canvas) : new Promise(resolve => { @@ -629,29 +629,29 @@ runtimeFunctions._resolveKeyPath = `const _resolveKeyPath = (obj, keyPath) => { runtimeFunctions.get = `const get = (obj, keyPath) => { const [root, key] = _resolveKeyPath(obj, keyPath); - return typeof root === 'undefined' - ? '' + return typeof root === 'undefined' + ? '' : root.get?.(key) ?? root[key]; }`; runtimeFunctions.set = `const set = (obj, keyPath, val) => { const [root, key] = _resolveKeyPath(obj, keyPath); - return typeof root === 'undefined' - ? '' + return typeof root === 'undefined' + ? '' : root.set?.(key) ?? (root[key] = val); }`; runtimeFunctions.remove = `const remove = (obj, keyPath) => { const [root, key] = _resolveKeyPath(obj, keyPath); - return typeof root === 'undefined' - ? '' + return typeof root === 'undefined' + ? '' : root.delete?.(key) ?? root.remove?.(key) ?? (delete root[key]); }`; runtimeFunctions.includes = `const includes = (obj, keyPath) => { const [root, key] = _resolveKeyPath(obj, keyPath); - return typeof root === 'undefined' - ? '' + return typeof root === 'undefined' + ? '' : root.has?.(key) ?? (key in root); }`; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 0a62d0c9c5a..ba7c9c908ba 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2622,6 +2622,7 @@ class Runtime extends EventEmitter { } const instance = this; const newThreads = []; + // Look up metadata for the relevant hat. const hatMeta = instance._hats[requestedHatOpcode]; From fa41c30ba5960065c11fc8f607f96deeb29c67c5 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Wed, 6 Aug 2025 19:23:16 -0700 Subject: [PATCH 03/18] Only store string indexes, fix clicking on a script --- src/extensions/jg_debugging/index.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 1367e47e853..a4b0fe0cf2b 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -166,14 +166,26 @@ class jgDebuggingBlocks { if (threads.length === 0) return; console.debug(opcode, fields, target, threads, caller_thread); - const starting_stack = !caller_thread ? new Set() : caller_thread.traceback; + const starting_stack = (() => { + if (!caller_thread) return new Set(); + if (caller_thread.traceback) return caller_thread.traceback; + + const guess_traceback_entry = { + target: caller_thread.target.id, + top_block: caller_thread.topBlock, + }; + return new Set().add(guess_traceback_entry); + })(); for (let thread of threads) { - const top_block = thread.blockContainer.getBlock(thread.topBlock); + const top_block = thread.topBlock; + const target = thread.target.id; + const stack_point = { - target: thread.target, + target, top_block, }; + const stack = new Set(starting_stack); stack.add(stack_point); thread.traceback = stack; @@ -248,11 +260,6 @@ class jgDebuggingBlocks { opcode: 'breakpoint', blockType: BlockType.COMMAND, }, - '---', - { - opcode: 'trace', - blockType: BlockType.COMMAND, - } ] }; } @@ -429,7 +436,9 @@ class jgDebuggingBlocks { this._addLog(text, "color: red;"); } - trace(args, util) {} + _renderTraceback(traceback) { + // TODO + } breakpoint() { this.runtime.pause(); From dd15f5bdaaa123330534e3250949831fa3c6c25f Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 7 Aug 2025 13:59:48 -0700 Subject: [PATCH 04/18] Start working on block jumping + render traceback as text --- src/extensions/jg_debugging/index.js | 126 ++++++++++++++++++--------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index a4b0fe0cf2b..562b795b7be 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -170,27 +170,27 @@ class jgDebuggingBlocks { if (!caller_thread) return new Set(); if (caller_thread.traceback) return caller_thread.traceback; - const guess_traceback_entry = { - target: caller_thread.target.id, - top_block: caller_thread.topBlock, - }; + const guess_traceback_entry = this._createStackTraceEntryFromThread(caller_thread); return new Set().add(guess_traceback_entry); })(); for (let thread of threads) { - const top_block = thread.topBlock; - const target = thread.target.id; - - const stack_point = { - target, - top_block, - }; + const stack_point = this._createStackTraceEntryFromThread(thread);; const stack = new Set(starting_stack); stack.add(stack_point); thread.traceback = stack; } }); + + this.isScratchBlocksReady = typeof ScratchBlocks === "object"; + this.ScratchBlocks = ScratchBlocks; + this.runtime.vm.on("workspaceUpdate", () => { + if (this.isScratchBlocksReady) return; + this.isScratchBlocksReady = typeof ScratchBlocks === "object"; + if (!this.isScratchBlocksReady) return; + this.ScratchBlocks = ScratchBlocks; + }); } /** @@ -271,7 +271,7 @@ class jgDebuggingBlocks { if (style) { logElement.style = `white-space: break-spaces; ${style}`; } - logElement.innerHTML = xmlEscape(log); + logElement.innerHTML = log; this.consoleLogs.scrollBy(0, 1000000); } _parseCommand(command) { @@ -398,46 +398,94 @@ class jgDebuggingBlocks { } log(args) { - const text = Cast.toString(args.INFO); + const text = xmlEscape(Cast.toString(args.INFO)); console.log(text); this._addLog(text); } warn(args) { - const text = Cast.toString(args.INFO); + const text = xmlEscape(Cast.toString(args.INFO)); console.warn(text); this._addLog(text, "color: yellow;"); } error(args, util) { - // create error stack - const stack = []; - const target = util.target; - const thread = util.thread; - if (thread.stackClick) { - stack.push('clicked blocks'); + const traceback = util.thread.traceback + ?? new Set().add(this._createStackTraceEntryFromThread(util.thread)); + // Assume we haven't yet touched this thread. + + const text = xmlEscape(Cast.toString(args.INFO)); + const log = `Error: ${text}\n` + + this._renderTraceback(traceback); + console.error(log); + this._addLog(log, "color: red;"); + } + + _renderTraceback(traceback) { + let initial_trace = Array.from(traceback).toReversed(); + let final_traceback = []; + for (let stack_element of initial_trace) { + const target_id = stack_element.target; + const block_id = stack_element.block_id; + + const target = this.runtime.targets.find(target => target.id == target_id); + const block = target.blocks.getBlock(block_id); + + const trace_text = "\t" + target.getName() + "::" + block.opcode + "@" + block.id; + // TODO: Replace the block ID with a cool link to the block instead. + // Note: Would require redoing the console to be HTML-safe. + + final_traceback.push(trace_text); } - const commandBlockId = thread.peekStack(); - const block = this._findBlockFromId(commandBlockId, target); - if (block) { - stack.push(`block ${block.opcode}`); - } else { - stack.push(`block ${commandBlockId}`); + return final_traceback.join("\n"); + } + + _createStackTraceEntryFromThread(thread) { + return { + target: thread.target.id, + block_id: thread.topBlock, + }; + } + + _jumpToTargetAndBlock(target_id, block_id) { + if (target_id != this.runtime.vm.editingTarget.id) { + this.runtime.vm.setEditingTarget(target_id); + this.runtime.vm.refreshWorkspace(); } - const eventBlock = this._findBlockFromId(thread.topBlock, target); - if (eventBlock) { - stack.push(`event ${eventBlock.opcode}`); - } else { - stack.push(`event ${thread.topBlock}`); + + if (!block_id || !this.isScratchBlocksReady) return; + + const workspace = this.ScratchBlocks.getMainWorkspace(); + const block = workspace.getBlockById(block_id); + + const root = block.getRootBlock(); + + let base = block; + while (base.getOutputShape() && base.getSurroundParent()) { + base = base.getSurroundParent(); } - stack.push(`sprite ${target.sprite.name}`); - const text = `Error: ${Cast.toString(args.INFO)}` - + `\n${stack.map(text => (`\tat ${text}`)).join("\n")}`; - console.error(text); - this._addLog(text, "color: red;"); - } + const offsetx = 32; + const offsety = 32; - _renderTraceback(traceback) { - // TODO + const epos = base.getRelativeToSurfaceXY(); + const rpos = root.getRelativeToSurfaceXY(); + const scale = workspace.scale; + const x = rpos.x * scale; + const y = epos.y * scale; + const xx = block.width + x; + const yy = block.height + y; + const s = workspace.getMetrics(); + + if ( + x < s.viewLeft + this.offsetX - 4 || + xx > s.viewLeft + s.viewWidth || + y < s.viewTop + this.offsetY - 4 || + yy > s.viewTop + s.viewHeight + ) { + const sx = x - s.contentLeft - this.offsetX; + const sy = y - s.contentTop - this.offsetY; + worspace.scrollbar.set(sx, sy); + } + this.ScratchBlocks?.hideChaff(); } breakpoint() { From 5f3780fa6c8b2441a661f6a48c6e974b809047c3 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 7 Aug 2025 14:25:08 -0700 Subject: [PATCH 05/18] Fix jumping to block (partly) --- src/extensions/jg_debugging/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 562b795b7be..5f37f4a0fb5 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -453,6 +453,8 @@ class jgDebuggingBlocks { if (!block_id || !this.isScratchBlocksReady) return; + console.log("b") + const workspace = this.ScratchBlocks.getMainWorkspace(); const block = workspace.getBlockById(block_id); @@ -483,7 +485,7 @@ class jgDebuggingBlocks { ) { const sx = x - s.contentLeft - this.offsetX; const sy = y - s.contentTop - this.offsetY; - worspace.scrollbar.set(sx, sy); + workspace.scrollbar.set(sx, sy); } this.ScratchBlocks?.hideChaff(); } From a9b1a832aac09df48e9a55c2679697b719cd0482 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 28 Aug 2025 19:22:41 -0700 Subject: [PATCH 06/18] Replace function here with the built-in ScratchBlocks workspace method --- src/extensions/jg_debugging/index.js | 35 +--------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 5f37f4a0fb5..23ca388e786 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -453,41 +453,8 @@ class jgDebuggingBlocks { if (!block_id || !this.isScratchBlocksReady) return; - console.log("b") - const workspace = this.ScratchBlocks.getMainWorkspace(); - const block = workspace.getBlockById(block_id); - - const root = block.getRootBlock(); - - let base = block; - while (base.getOutputShape() && base.getSurroundParent()) { - base = base.getSurroundParent(); - } - - const offsetx = 32; - const offsety = 32; - - const epos = base.getRelativeToSurfaceXY(); - const rpos = root.getRelativeToSurfaceXY(); - const scale = workspace.scale; - const x = rpos.x * scale; - const y = epos.y * scale; - const xx = block.width + x; - const yy = block.height + y; - const s = workspace.getMetrics(); - - if ( - x < s.viewLeft + this.offsetX - 4 || - xx > s.viewLeft + s.viewWidth || - y < s.viewTop + this.offsetY - 4 || - yy > s.viewTop + s.viewHeight - ) { - const sx = x - s.contentLeft - this.offsetX; - const sy = y - s.contentTop - this.offsetY; - workspace.scrollbar.set(sx, sy); - } - this.ScratchBlocks?.hideChaff(); + workspace.centerOnBlock(block_id); } breakpoint() { From 8db4dd270804a744b841df2db0435168fd31f0d1 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 28 Aug 2025 20:46:28 -0700 Subject: [PATCH 07/18] Move traceback handling internal This could allow for more stuff to take advantage of the system, as well, it makes it a bit easier to manage since it won't require everything to have events attached. --- src/engine/runtime.js | 15 +++++++++- src/engine/thread.js | 12 ++++++-- src/extensions/jg_debugging/index.js | 43 ++++++++-------------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f46b423727f..75d0ce69e57 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2442,6 +2442,7 @@ class Runtime extends EventEmitter { thread.target = target; thread.stackClick = Boolean(opts && opts.stackClick); thread.updateMonitor = Boolean(opts && opts.updateMonitor); + thread.blockContainer = thread.updateMonitor ? this.monitorBlocks : ((opts && opts.targetBlockLocation) || target.blocks); @@ -2452,6 +2453,14 @@ class Runtime extends EventEmitter { this.threadMap.set(thread.getId(), thread); } + // pm: Don't append a traceback to monitor threads. + if (!opts?.updateMonitor) { + thread.traceback = new Set(opts?.parent_thread?.traceback ?? []).add({ + target: thread.target.id, + block_id: thread.topBlock + }); + } + // tw: compile new threads. Do not attempt to compile monitor threads. if (!(opts && opts.updateMonitor) && this.compilerOptions.enabled) { thread.tryCompile(); @@ -2690,7 +2699,9 @@ class Runtime extends EventEmitter { } } // Start the thread with this top block. - newThreads.push(this._pushThread(topBlockId, target)); + newThreads.push(this._pushThread(topBlockId, target, { + parent_thread: optParentThread + })); }, optTarget); // For compatibility with Scratch 2, edge triggered hats need to be processed before // threads are stepped. See ScratchRuntime.as for original implementation @@ -2709,6 +2720,8 @@ class Runtime extends EventEmitter { execute(this.sequencer, thread); thread.goToNextBlock(); } + + }); this.emit(Runtime.HATS_STARTED, requestedHatOpcode, optMatchFields, optTarget, newThreads, optParentThread diff --git a/src/engine/thread.js b/src/engine/thread.js index 8aeb0512f3f..2a7f96fb411 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -218,11 +218,17 @@ class Thread { this.compatibilityStackFrame = null; /** - * Thread vars: for allowing a compiled version of the + * Thread vars: for allowing a compiled version of the * LilyMakesThings Thread Variables extension * @type {Object} */ this.variables = Object.create(null); + + /** + * Set containing parental history of this thread. + * @type {Set} + */ + this.traceback = new Set(); } /** @@ -263,7 +269,7 @@ class Thread { /** * Thread status for a paused thread. - * Thread is in this state when it has been told to pause and needs to pause + * Thread is in this state when it has been told to pause and needs to pause * any new yields from the compiler * @const */ @@ -544,7 +550,7 @@ class Thread { for (const procedureCode of Object.keys(result.procedures)) { this.procedures[procedureCode] = result.procedures[procedureCode](this); } - + this.generator = result.startingFunction(this)(); this.executableHat = result.executableHat; diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 23ca388e786..62a30f6bfb2 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -162,27 +162,6 @@ class jgDebuggingBlocks { this.commandSet = {}; this.commandExplanations = {}; - runtime.on('HATS_STARTED', (opcode, fields, target, threads, caller_thread) => { - if (threads.length === 0) return; - console.debug(opcode, fields, target, threads, caller_thread); - - const starting_stack = (() => { - if (!caller_thread) return new Set(); - if (caller_thread.traceback) return caller_thread.traceback; - - const guess_traceback_entry = this._createStackTraceEntryFromThread(caller_thread); - return new Set().add(guess_traceback_entry); - })(); - - for (let thread of threads) { - const stack_point = this._createStackTraceEntryFromThread(thread);; - - const stack = new Set(starting_stack); - stack.add(stack_point); - thread.traceback = stack; - } - }); - this.isScratchBlocksReady = typeof ScratchBlocks === "object"; this.ScratchBlocks = ScratchBlocks; this.runtime.vm.on("workspaceUpdate", () => { @@ -408,9 +387,18 @@ class jgDebuggingBlocks { this._addLog(text, "color: yellow;"); } error(args, util) { - const traceback = util.thread.traceback - ?? new Set().add(this._createStackTraceEntryFromThread(util.thread)); - // Assume we haven't yet touched this thread. + const traceback = new Set(util.thread.traceback); + const current_trace_stack = { + target: util.target.id, + block_id: util.thread.peekStack() + }; + const latest_trace = [...traceback][traceback.size - 1]; + // Do not add again if it's already in the traceback. + if ( + latest_trace.block_id !== current_trace_stack.block_id && + latest_trace.target !== current_trace_stack.target + ) + traceback.add(current_trace_stack); const text = xmlEscape(Cast.toString(args.INFO)); const log = `Error: ${text}\n` + @@ -438,13 +426,6 @@ class jgDebuggingBlocks { return final_traceback.join("\n"); } - _createStackTraceEntryFromThread(thread) { - return { - target: thread.target.id, - block_id: thread.topBlock, - }; - } - _jumpToTargetAndBlock(target_id, block_id) { if (target_id != this.runtime.vm.editingTarget.id) { this.runtime.vm.setEditingTarget(target_id); From a71cc09e5c3cc46af0b9117025fea9aa739c755b Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 28 Aug 2025 21:55:35 -0700 Subject: [PATCH 08/18] Move traceback handling back into jg_debugging It's under a different event, now, which should be called more often. --- src/engine/runtime.js | 10 +--------- src/extensions/jg_debugging/index.js | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 75d0ce69e57..44de98bde04 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2453,20 +2453,12 @@ class Runtime extends EventEmitter { this.threadMap.set(thread.getId(), thread); } - // pm: Don't append a traceback to monitor threads. - if (!opts?.updateMonitor) { - thread.traceback = new Set(opts?.parent_thread?.traceback ?? []).add({ - target: thread.target.id, - block_id: thread.topBlock - }); - } - // tw: compile new threads. Do not attempt to compile monitor threads. if (!(opts && opts.updateMonitor) && this.compilerOptions.enabled) { thread.tryCompile(); } - this.emit(Runtime.THREAD_STARTED, thread); + this.emit(Runtime.THREAD_STARTED, thread, opts); return thread; } diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 62a30f6bfb2..23601339514 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -162,6 +162,14 @@ class jgDebuggingBlocks { this.commandSet = {}; this.commandExplanations = {}; + runtime.on("THREAD_STARTED", (thread, options) => { + if (options?.updateMonitor) return; + thread.traceback = new Set(options?.parent_thread?.traceback ?? []).add({ + target: thread.target.id, + block_id: thread.topBlock, + }); + }); + this.isScratchBlocksReady = typeof ScratchBlocks === "object"; this.ScratchBlocks = ScratchBlocks; this.runtime.vm.on("workspaceUpdate", () => { From 7f4edbf244d9f2cd7d515e49084072fd6b73e7f5 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 27 Nov 2025 16:29:37 -0800 Subject: [PATCH 09/18] JSGen shenanigans to get procedure tracing working --- src/compiler/jsgen.js | 5 ++++- src/extensions/jg_debugging/index.js | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index e88c5c735c1..551922ee7c7 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -355,7 +355,7 @@ class Frame { */ this.isLastBlock = false; - this.overrideLoop = overrideLoop + this.overrideLoop = overrideLoop /** * General important data that needs to be carried down from other threads. @@ -1193,6 +1193,9 @@ class JSGenerator { this.source += `yield* executeInCompatibilityLayer(${inputs}, ${blockFunction}, ${this.isWarp}, false, ${blockId});\n`; break; } + case 'literal': + this.source += node.literal; + break; case 'compat': { // If the last command in a loop returns a promise, immediately continue to the next iteration. // If you don't do this, the loop effectively yields twice per iteration and will run at half-speed. diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 23601339514..58f3b8489f0 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -162,14 +162,6 @@ class jgDebuggingBlocks { this.commandSet = {}; this.commandExplanations = {}; - runtime.on("THREAD_STARTED", (thread, options) => { - if (options?.updateMonitor) return; - thread.traceback = new Set(options?.parent_thread?.traceback ?? []).add({ - target: thread.target.id, - block_id: thread.topBlock, - }); - }); - this.isScratchBlocksReady = typeof ScratchBlocks === "object"; this.ScratchBlocks = ScratchBlocks; this.runtime.vm.on("workspaceUpdate", () => { @@ -178,6 +170,25 @@ class jgDebuggingBlocks { if (!this.isScratchBlocksReady) return; this.ScratchBlocks = ScratchBlocks; }); + + runtime.on("THREAD_STARTED", (thread, options) => { + if (options?.updateMonitor) return; + thread.traceback = new Set(options?.parent_thread?.traceback ?? []); + }); + + const _jsgen_compile = vm.exports.JSGenerator.prototype.compile; + vm.exports.JSGenerator.prototype.compile = function() { + this.script.stack.push({ + kind: 'literal', + literal: 'thread.traceback = jgDebugging__TracebackT;' + }) + this.source += `var jgDebugging__TracebackT = new Set(thread.traceback); + thread.traceback = thread.traceback.add({ + target: thread.target.id, + block_id: "${this.script.topBlockId}", + });`; + return _jsgen_compile.call(this); + }; } /** From e7be01ea47a69531126b258dfb7a0e2d3f8160ee Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 27 Nov 2025 17:22:45 -0800 Subject: [PATCH 10/18] Add link to jump to blocks in project Known issue: if a singular block is clicked, either in the palette or in the editing area, the output to the debugger will contain two entries to the same block. I was having issues getting it to only have one or the other: this is a tradeoff I think is fair, as it's far more important to know where issues are going on in your codebase than to not see duplicated entries --- src/extensions/jg_debugging/index.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 58f3b8489f0..315b125e466 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -406,18 +406,11 @@ class jgDebuggingBlocks { this._addLog(text, "color: yellow;"); } error(args, util) { - const traceback = new Set(util.thread.traceback); const current_trace_stack = { target: util.target.id, block_id: util.thread.peekStack() }; - const latest_trace = [...traceback][traceback.size - 1]; - // Do not add again if it's already in the traceback. - if ( - latest_trace.block_id !== current_trace_stack.block_id && - latest_trace.target !== current_trace_stack.target - ) - traceback.add(current_trace_stack); + const traceback = new Set(util.thread.traceback).add(current_trace_stack); const text = xmlEscape(Cast.toString(args.INFO)); const log = `Error: ${text}\n` + @@ -436,9 +429,22 @@ class jgDebuggingBlocks { const target = this.runtime.targets.find(target => target.id == target_id); const block = target.blocks.getBlock(block_id); - const trace_text = "\t" + target.getName() + "::" + block.opcode + "@" + block.id; - // TODO: Replace the block ID with a cool link to the block instead. - // Note: Would require redoing the console to be HTML-safe. + + if (block === undefined) { + final_traceback.push("\tanonymous::" + block_id + "@anonymous"); + continue; + } + + const target_name = xmlEscape(target.getName()); + const block_name = block.opcode + "@" + block_id; + + const block_link = +`${block_name}`; + + const trace_text = "\t" + target_name + "::" + block_link; final_traceback.push(trace_text); } From ecd58b6576b0deb6465b3f060922ba264832aec4 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 21:45:04 -0800 Subject: [PATCH 11/18] Use camel case on the parent-thread for the hats started event --- src/engine/runtime.js | 2 +- src/extensions/jg_debugging/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6acb6ba7c7e..9bbfe587f3f 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2734,7 +2734,7 @@ class Runtime extends EventEmitter { } // Start the thread with this top block. newThreads.push(this._pushThread(topBlockId, target, { - parent_thread: optParentThread + parentThread: optParentThread })); }, optTarget); // For compatibility with Scratch 2, edge triggered hats need to be processed before diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 315b125e466..fb8a487810d 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -173,7 +173,7 @@ class jgDebuggingBlocks { runtime.on("THREAD_STARTED", (thread, options) => { if (options?.updateMonitor) return; - thread.traceback = new Set(options?.parent_thread?.traceback ?? []); + thread.traceback = new Set(options?.parentThread?.traceback ?? []); }); const _jsgen_compile = vm.exports.JSGenerator.prototype.compile; From 43c5bc43c54916a4b330d30adc777ee972c57053 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 21:46:46 -0800 Subject: [PATCH 12/18] Camel case the block id in the traceback --- src/extensions/jg_debugging/index.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index fb8a487810d..c9ca37bf856 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -185,7 +185,7 @@ class jgDebuggingBlocks { this.source += `var jgDebugging__TracebackT = new Set(thread.traceback); thread.traceback = thread.traceback.add({ target: thread.target.id, - block_id: "${this.script.topBlockId}", + blockId: "${this.script.topBlockId}", });`; return _jsgen_compile.call(this); }; @@ -408,7 +408,7 @@ class jgDebuggingBlocks { error(args, util) { const current_trace_stack = { target: util.target.id, - block_id: util.thread.peekStack() + blockId: util.thread.peekStack() }; const traceback = new Set(util.thread.traceback).add(current_trace_stack); @@ -424,24 +424,24 @@ class jgDebuggingBlocks { let final_traceback = []; for (let stack_element of initial_trace) { const target_id = stack_element.target; - const block_id = stack_element.block_id; + const blockId = stack_element.blockId; const target = this.runtime.targets.find(target => target.id == target_id); - const block = target.blocks.getBlock(block_id); + const block = target.blocks.getBlock(blockId); if (block === undefined) { - final_traceback.push("\tanonymous::" + block_id + "@anonymous"); + final_traceback.push("\tanonymous::" + blockId + "@anonymous"); continue; } const target_name = xmlEscape(target.getName()); - const block_name = block.opcode + "@" + block_id; + const block_name = block.opcode + "@" + blockId; const block_link = `${block_name}`; const trace_text = "\t" + target_name + "::" + block_link; @@ -451,16 +451,16 @@ class jgDebuggingBlocks { return final_traceback.join("\n"); } - _jumpToTargetAndBlock(target_id, block_id) { + _jumpToTargetAndBlock(target_id, blockId) { if (target_id != this.runtime.vm.editingTarget.id) { this.runtime.vm.setEditingTarget(target_id); this.runtime.vm.refreshWorkspace(); } - if (!block_id || !this.isScratchBlocksReady) return; + if (!blockId || !this.isScratchBlocksReady) return; const workspace = this.ScratchBlocks.getMainWorkspace(); - workspace.centerOnBlock(block_id); + workspace.centerOnBlock(blockId); } breakpoint() { From ea25c6c737bb5f1ed509d5242c7618a4406d2ac2 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 21:55:37 -0800 Subject: [PATCH 13/18] Don't render in HTML for the console print --- src/extensions/jg_debugging/index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index c9ca37bf856..fae3df38ee3 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -413,13 +413,12 @@ class jgDebuggingBlocks { const traceback = new Set(util.thread.traceback).add(current_trace_stack); const text = xmlEscape(Cast.toString(args.INFO)); - const log = `Error: ${text}\n` + - this._renderTraceback(traceback); - console.error(log); - this._addLog(log, "color: red;"); + const log = "Error:" + text + "\n"; + this._addLog(log + this._renderTraceback(traceback), "color: red;"); + console.error(log + this._renderTraceback(traceback, true)); } - _renderTraceback(traceback) { + _renderTraceback(traceback, disableHTML = false) { let initial_trace = Array.from(traceback).toReversed(); let final_traceback = []; for (let stack_element of initial_trace) { @@ -438,13 +437,13 @@ class jgDebuggingBlocks { const target_name = xmlEscape(target.getName()); const block_name = block.opcode + "@" + blockId; - const block_link = + const block_ref = disableHTML ? block_name : `${block_name}`; - const trace_text = "\t" + target_name + "::" + block_link; + const trace_text = "\t" + target_name + "::" + block_ref; final_traceback.push(trace_text); } From 1b771e6d6e28824cb0195c1190ae1988d91cf97e Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 22:15:20 -0800 Subject: [PATCH 14/18] Add traceback to warning --- src/extensions/jg_debugging/index.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index fae3df38ee3..2cd5997c23f 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -400,10 +400,16 @@ class jgDebuggingBlocks { console.log(text); this._addLog(text); } - warn(args) { - const text = xmlEscape(Cast.toString(args.INFO)); - console.warn(text); - this._addLog(text, "color: yellow;"); + warn(args, util) { + const current_trace_stack = { + target: util.target.id, + blockId: util.thread.peekStack() + }; + const traceback = new Set(util.thread.traceback).add(current_trace_stack); + + const log = "Warning: " + xmlEscape(Cast.toString(args.INFO)) + "\n"; + this._addLog(log + this._renderTraceback(traceback, { linkColor: "#fb0" }), "color: yellow;"); + console.error(log + this._renderTraceback(traceback, { linkColor: "#fb0", disableHTML: true })); } error(args, util) { const current_trace_stack = { @@ -412,13 +418,14 @@ class jgDebuggingBlocks { }; const traceback = new Set(util.thread.traceback).add(current_trace_stack); - const text = xmlEscape(Cast.toString(args.INFO)); - const log = "Error:" + text + "\n"; + const log = "Error: " + xmlEscape(Cast.toString(args.INFO)) + "\n"; this._addLog(log + this._renderTraceback(traceback), "color: red;"); - console.error(log + this._renderTraceback(traceback, true)); + console.error(log + this._renderTraceback(traceback, { disableHTML: true })); } - _renderTraceback(traceback, disableHTML = false) { + _renderTraceback(traceback, opts={}) { + const disableHTML = opts?.disableHTML ?? false; + const linkColor = opts?.linkColor ?? "#f0b"; let initial_trace = Array.from(traceback).toReversed(); let final_traceback = []; for (let stack_element of initial_trace) { @@ -439,7 +446,7 @@ class jgDebuggingBlocks { const block_ref = disableHTML ? block_name : `${block_name}`; From 80771f9b93c51de38a9012e99924358d5f5a8e4b Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 22:27:25 -0800 Subject: [PATCH 15/18] Replace block name with procCode for stack elements that represent proc defs. --- src/extensions/jg_debugging/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 2cd5997c23f..bb229a0e676 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -181,11 +181,15 @@ class jgDebuggingBlocks { this.script.stack.push({ kind: 'literal', literal: 'thread.traceback = jgDebugging__TracebackT;' - }) + }); + + const proc_code = this.isProcedure ? `procCode: "${this.script.procedureCode}",` : ""; + this.source += `var jgDebugging__TracebackT = new Set(thread.traceback); thread.traceback = thread.traceback.add({ target: thread.target.id, blockId: "${this.script.topBlockId}", + ${proc_code} });`; return _jsgen_compile.call(this); }; @@ -431,6 +435,7 @@ class jgDebuggingBlocks { for (let stack_element of initial_trace) { const target_id = stack_element.target; const blockId = stack_element.blockId; + const isProcedure = stack_element.hasOwnProperty("procCode"); const target = this.runtime.targets.find(target => target.id == target_id); const block = target.blocks.getBlock(blockId); @@ -442,7 +447,8 @@ class jgDebuggingBlocks { } const target_name = xmlEscape(target.getName()); - const block_name = block.opcode + "@" + blockId; + const block_name = + (isProcedure ? stack_element.procCode : block.opcode) + "@" + blockId; const block_ref = disableHTML ? block_name : ` Date: Sun, 7 Dec 2025 22:44:43 -0800 Subject: [PATCH 16/18] Allow tracing with scripts (ext) and new thread (block). --- src/extensions/jg_scripts/index.js | 15 +++++++++++++-- src/extensions/pm_controlsExpansion/index.js | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/extensions/jg_scripts/index.js b/src/extensions/jg_scripts/index.js index 03151e4f762..d508aac057a 100644 --- a/src/extensions/jg_scripts/index.js +++ b/src/extensions/jg_scripts/index.js @@ -196,7 +196,12 @@ class JgScriptsBlocks { if (!thread && index < blocks.length) { const thisStack = blocks[index]; if (thisStack.target.blocks.getBlock(thisStack.stack) !== undefined) { - util.stackFrame.JGthread = this.runtime._pushThread(thisStack.stack, thisStack.target, { stackClick: false }); + util.stackFrame.JGthread = this.runtime._pushThread( + thisStack.stack, + thisStack.target, { + stackClick: false, + parentThread: util.thread, + }); util.stackFrame.JGthread.scriptData = data; util.stackFrame.JGthread.target = target; util.stackFrame.JGthread.tryCompile(); // update thread @@ -225,7 +230,13 @@ class JgScriptsBlocks { if (!thread && index < blocks.length) { const thisStack = blocks[index]; if (thisStack.target.blocks.getBlock(thisStack.stack) !== undefined) { - util.stackFrame.JGthread = this.runtime._pushThread(thisStack.stack, thisStack.target, { stackClick: false }); + util.stackFrame.JGthread = this.runtime._pushThread( + thisStack.stack, + thisStack.target, + { + stackClick: false, + parentThread: util.thread, + }); util.stackFrame.JGthread.scriptData = data; util.stackFrame.JGthread.target = target; util.stackFrame.JGthread.tryCompile(); // update thread diff --git a/src/extensions/pm_controlsExpansion/index.js b/src/extensions/pm_controlsExpansion/index.js index d432b7e29bd..998f0e02f71 100644 --- a/src/extensions/pm_controlsExpansion/index.js +++ b/src/extensions/pm_controlsExpansion/index.js @@ -259,7 +259,7 @@ class pmControlsExpansion { util.startBranch(2, false); } } - + ifElseIfElse (args, util) { const condition1 = Cast.toBoolean(args.CONDITION1); const condition2 = Cast.toBoolean(args.CONDITION2); @@ -271,7 +271,7 @@ class pmControlsExpansion { util.startBranch(3, false); } } - + restartFromTheTop() { return; // doesnt work in compat mode } @@ -282,7 +282,7 @@ class pmControlsExpansion { util.sequencer.runtime._pushThread( util.thread.target.blocks.getBranch(util.thread.peekStack(), 0), util.target, - {} + { parentThread: util.thread } ); } } From 2a298baf34ae7a28bbcbcd458c2a405345b22b77 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 7 Dec 2025 22:52:57 -0800 Subject: [PATCH 17/18] Use a local variable name, here. --- src/extensions/jg_debugging/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index bb229a0e676..1f311194f14 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -178,14 +178,15 @@ class jgDebuggingBlocks { const _jsgen_compile = vm.exports.JSGenerator.prototype.compile; vm.exports.JSGenerator.prototype.compile = function() { + const old_trace = this.localVariables.next(); this.script.stack.push({ kind: 'literal', - literal: 'thread.traceback = jgDebugging__TracebackT;' + literal: `thread.traceback = ${old_trace};` }); const proc_code = this.isProcedure ? `procCode: "${this.script.procedureCode}",` : ""; - this.source += `var jgDebugging__TracebackT = new Set(thread.traceback); + this.source += `var ${old_trace} = new Set(thread.traceback); thread.traceback = thread.traceback.add({ target: thread.target.id, blockId: "${this.script.topBlockId}", From 4d52e49d57155f648b8c19dc0b65fd8db5afea8a Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 25 Dec 2025 16:51:26 -0800 Subject: [PATCH 18/18] Add padding to debugging window --- src/compiler/jsgen.js | 2 +- src/extensions/jg_debugging/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 551922ee7c7..9a897384a87 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -355,7 +355,7 @@ class Frame { */ this.isLastBlock = false; - this.overrideLoop = overrideLoop + this.overrideLoop = overrideLoop; /** * General important data that needs to be carried down from other threads. diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 1f311194f14..9cc0b6b0ca5 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -72,7 +72,7 @@ class jgDebuggingBlocks { + 'position: absolute; left: 0px; top: 2rem;' + 'color: white; cursor: text; overflow: auto;' + 'background: transparent; outline: unset !important;' - + 'border: 0; margin: 0; padding: 0; font-family: monospace;' + + 'border: 0; margin: 0; padding: 1rem; font-family: monospace;' + 'display: flex; flex-direction: column; align-items: flex-start;' + 'z-index: 1000005; user-select: text;';