From 16003787e41874251d2c7cf3f1e219c0110a25a1 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Wed, 6 Aug 2025 16:00:34 -0700 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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;'; From 6899df30fc65a916efa53ab292ca6af39925b2b1 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Thu, 1 Jan 2026 22:00:32 -0800 Subject: [PATCH 19/22] Fix logging while spoofing. --- src/compiler/jsgen.js | 4 ++++ src/extensions/jg_debugging/index.js | 16 +++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 9a897384a87..a84e8d98a6a 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -1477,6 +1477,8 @@ class JSGenerator { // save the original target const originalTarget = this.localVariables.next(); this.source += `const ${originalTarget} = target;\n`; + + this.source += `if (!thread.spoofing) { thread.spoofOrigin = target; }`; // pm: unknown behavior may appear so lets use try catch this.source += `try {\n`; // set target @@ -1505,6 +1507,7 @@ class JSGenerator { this.source += `thread.target = ${originalTarget};\n`; this.source += `thread.spoofing = ${alreadySpoofing};\n`; this.source += `thread.spoofTarget = ${alreadySpoofTarget};\n`; + this.source += `if (!thread.spoofing) { thread.spoofOrigin = null; }`; this.source += `}\n`; this.source += `} catch (e) {\nconsole.log('as sprite function failed;', e);\n`; @@ -1513,6 +1516,7 @@ class JSGenerator { this.source += `thread.target = ${originalTarget};\n`; this.source += `thread.spoofing = ${alreadySpoofing};\n`; this.source += `thread.spoofTarget = ${alreadySpoofTarget};\n`; + this.source += `if (!thread.spoofing) { thread.spoofOrigin = null; }`; this.source += `}\n`; break; diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 9cc0b6b0ca5..7c001cc6327 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -184,13 +184,13 @@ class jgDebuggingBlocks { literal: `thread.traceback = ${old_trace};` }); - const proc_code = this.isProcedure ? `procCode: "${this.script.procedureCode}",` : ""; + const proc_code = this.isProcedure ? `"${this.script.procedureCode}"` : "null"; this.source += `var ${old_trace} = new Set(thread.traceback); thread.traceback = thread.traceback.add({ target: thread.target.id, blockId: "${this.script.topBlockId}", - ${proc_code} + procCode: ${proc_code}, });`; return _jsgen_compile.call(this); }; @@ -407,8 +407,9 @@ class jgDebuggingBlocks { } warn(args, util) { const current_trace_stack = { - target: util.target.id, - blockId: util.thread.peekStack() + target: !!util.thread.spoofing ? util.thread.spoofOrigin.id : util.target.id, + blockId: util.thread.peekStack(), + procCode: null, }; const traceback = new Set(util.thread.traceback).add(current_trace_stack); @@ -418,8 +419,9 @@ class jgDebuggingBlocks { } error(args, util) { const current_trace_stack = { - target: util.target.id, - blockId: util.thread.peekStack() + target: !!util.thread.spoofing ? util.thread.spoofOrigin.id : util.target.id, + blockId: util.thread.peekStack(), + procCode: null, }; const traceback = new Set(util.thread.traceback).add(current_trace_stack); @@ -436,7 +438,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 isProcedure = stack_element.procCode !== null; const target = this.runtime.targets.find(target => target.id == target_id); const block = target.blocks.getBlock(blockId); From 4b058e28ca11ec950c633d511bb9f984bdfed0ab Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sat, 3 Jan 2026 16:36:15 -0800 Subject: [PATCH 20/22] Add script errors to console with traceback attached --- src/compiler/jsgen.js | 3 ++- src/extensions/jg_debugging/index.js | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index a84e8d98a6a..1eab836a5d8 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -2278,7 +2278,8 @@ class JSGenerator { script += 'console.error(err);'; script += `runtime.emit("BLOCK_STACK_ERROR", {`; script += `id:"${sanitize(this.script.topBlockId)}",`; - script += `value:String(err)`; + script += `value:String(err),`; + script += `thread,` script += `});\n`; script += '}\n'; if (!this.isProcedure) { diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 7c001cc6327..b304720fe26 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -164,7 +164,7 @@ class jgDebuggingBlocks { this.isScratchBlocksReady = typeof ScratchBlocks === "object"; this.ScratchBlocks = ScratchBlocks; - this.runtime.vm.on("workspaceUpdate", () => { + runtime.vm.on("workspaceUpdate", () => { if (this.isScratchBlocksReady) return; this.isScratchBlocksReady = typeof ScratchBlocks === "object"; if (!this.isScratchBlocksReady) return; @@ -176,6 +176,11 @@ class jgDebuggingBlocks { thread.traceback = new Set(options?.parentThread?.traceback ?? []); }); + runtime.on("BLOCK_STACK_ERROR", ({ value, thread }) => { + const log = "\"" + xmlEscape(value) + "\" thrown in script:\n"; + this._addLog(log + this._renderTraceback(thread.traceback), "color: red;"); + }); + const _jsgen_compile = vm.exports.JSGenerator.prototype.compile; vm.exports.JSGenerator.prototype.compile = function() { const old_trace = this.localVariables.next(); From c7252c8c56fcdb7896960d71c7c7e294659648c8 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 11 Jan 2026 13:03:59 -0800 Subject: [PATCH 21/22] Fixup warning Use `console.warn` not `console.error`, also remove link color from rendering opts: it's unneeded. --- src/extensions/jg_debugging/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index b304720fe26..9ea1d57b75d 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -420,7 +420,7 @@ class jgDebuggingBlocks { 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 })); + console.warn(log + this._renderTraceback(traceback, { disableHTML: true })); } error(args, util) { const current_trace_stack = { From b64d0bd4af0b73cd2a93665cf381036543c6ba21 Mon Sep 17 00:00:00 2001 From: Steve0Greatness Date: Sun, 11 Jan 2026 13:32:14 -0800 Subject: [PATCH 22/22] Support jwLambda Hopefully this overhead is fairly minimal. I don't think there's a way to figure it out, otherwise, since it discards its otherwise discards its origin thread fairly quickly. This just makes it so that JW Lambda says when it's running. Pretty much. --- src/extensions/jg_debugging/index.js | 12 +++++++++--- src/extensions/jwLambda/index.js | 9 ++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/extensions/jg_debugging/index.js b/src/extensions/jg_debugging/index.js index 9ea1d57b75d..53e4c3dde2b 100644 --- a/src/extensions/jg_debugging/index.js +++ b/src/extensions/jg_debugging/index.js @@ -412,7 +412,7 @@ class jgDebuggingBlocks { } warn(args, util) { const current_trace_stack = { - target: !!util.thread.spoofing ? util.thread.spoofOrigin.id : util.target.id, + target: this.figure_block_target(util.thread), blockId: util.thread.peekStack(), procCode: null, }; @@ -424,7 +424,7 @@ class jgDebuggingBlocks { } error(args, util) { const current_trace_stack = { - target: !!util.thread.spoofing ? util.thread.spoofOrigin.id : util.target.id, + target: this.figure_block_target(util.thread), blockId: util.thread.peekStack(), procCode: null, }; @@ -434,7 +434,13 @@ class jgDebuggingBlocks { this._addLog(log + this._renderTraceback(traceback), "color: red;"); console.error(log + this._renderTraceback(traceback, { disableHTML: true })); } - + figure_block_target(thread) { + if (thread.spoofing) + return thread.spoofOrigin.id; + if (!!thread._jwLambdaRunning) + return thread._jwLambdaRunning.parentTarget.id; + return thread.target.id; + } _renderTraceback(traceback, opts={}) { const disableHTML = opts?.disableHTML ?? false; const linkColor = opts?.linkColor ?? "#f0b"; diff --git a/src/extensions/jwLambda/index.js b/src/extensions/jwLambda/index.js index 5d0bddd7458..f1b0d5e5bde 100644 --- a/src/extensions/jwLambda/index.js +++ b/src/extensions/jwLambda/index.js @@ -40,6 +40,7 @@ class LambdaType { this.func = func this.proc = thread ? thread.procedures : {} this.timesExecuted = 0 + this.parentTarget = thread ? thread.target : null } static toLambda(x) { @@ -67,10 +68,12 @@ class LambdaType { execute = function* (arg, thread, target, runtime, stage) { thread._jwLambdaArgument ??= [] thread._jwLambdaArgument.push(arg) + thread._jwLambdaRunning = this; if (this.proc) thread.procedures = {...this.proc, ...thread.procedures} this.timesExecuted++ let output = (yield* this.func(arg, thread, target, runtime, stage, this) ?? "") thread._jwLambdaArgument.pop() + thread._jwLambdaRunning = null; return output } } @@ -103,8 +106,8 @@ class Extension { vm.jwLambda = Lambda vm.runtime.registerSerializer( - "jwLambda", - v => null, + "jwLambda", + v => null, v => new Lambda.Type() ); vm.runtime.registerCompiledExtensionBlocks('jwLambda', this.getCompileInfo()); @@ -313,4 +316,4 @@ class Extension { } } -module.exports = Extension \ No newline at end of file +module.exports = Extension