diff --git a/src/compiler/jsexecute.js b/src/compiler/jsexecute.js index 2037bf162a4..47b576deff0 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,8 +629,8 @@ 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]; }`; @@ -643,15 +643,15 @@ runtimeFunctions.set = `const set = (obj, keyPath, 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/compiler/jsgen.js b/src/compiler/jsgen.js index e88c5c735c1..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. @@ -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/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 1ac3d646d63..9bbfe587f3f 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2484,6 +2484,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); @@ -2499,7 +2500,7 @@ class Runtime extends EventEmitter { thread.tryCompile(); } - this.emit(Runtime.THREAD_STARTED, thread); + this.emit(Runtime.THREAD_STARTED, thread, opts); return thread; } @@ -2667,15 +2668,17 @@ 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; } const instance = this; const newThreads = []; + // Look up metadata for the relevant hat. const hatMeta = instance._hats[requestedHatOpcode]; @@ -2730,7 +2733,9 @@ class Runtime extends EventEmitter { } } // Start the thread with this top block. - newThreads.push(this._pushThread(topBlockId, target)); + newThreads.push(this._pushThread(topBlockId, target, { + parentThread: optParentThread + })); }, optTarget); // For compatibility with Scratch 2, edge triggered hats need to be processed before // threads are stepped. See ScratchRuntime.as for original implementation @@ -2749,8 +2754,12 @@ class Runtime extends EventEmitter { execute(this.sequencer, thread); 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/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 317e56780bf..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;'; @@ -161,6 +161,39 @@ class jgDebuggingBlocks { this._logs = []; this.commandSet = {}; this.commandExplanations = {}; + + 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; + }); + + runtime.on("THREAD_STARTED", (thread, options) => { + if (options?.updateMonitor) return; + thread.traceback = new Set(options?.parentThread?.traceback ?? []); + }); + + 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 = ${old_trace};` + }); + + const proc_code = this.isProcedure ? `procCode: "${this.script.procedureCode}",` : ""; + + this.source += `var ${old_trace} = new Set(thread.traceback); + thread.traceback = thread.traceback.add({ + target: thread.target.id, + blockId: "${this.script.topBlockId}", + ${proc_code} + });`; + return _jsgen_compile.call(this); + }; } /** @@ -229,7 +262,7 @@ class jgDebuggingBlocks { { opcode: 'breakpoint', blockType: BlockType.COMMAND, - } + }, ] }; } @@ -241,7 +274,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) { @@ -368,42 +401,79 @@ 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); - 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) { - // create error stack - const stack = []; - const target = util.target; - const thread = util.thread; - if (thread.stackClick) { - stack.push('clicked blocks'); - } - const commandBlockId = thread.peekStack(); - const block = this._findBlockFromId(commandBlockId, target); - if (block) { - stack.push(`block ${block.opcode}`); - } else { - stack.push(`block ${commandBlockId}`); + 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 = "Error: " + xmlEscape(Cast.toString(args.INFO)) + "\n"; + this._addLog(log + this._renderTraceback(traceback), "color: red;"); + console.error(log + this._renderTraceback(traceback, { disableHTML: true })); + } + + _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) { + 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); + + + if (block === undefined) { + final_traceback.push("\tanonymous::" + blockId + "@anonymous"); + continue; + } + + const target_name = xmlEscape(target.getName()); + const block_name = + (isProcedure ? stack_element.procCode : block.opcode) + "@" + blockId; + + const block_ref = disableHTML ? block_name : +`${block_name}`; + + const trace_text = "\t" + target_name + "::" + block_ref; + + final_traceback.push(trace_text); } - const eventBlock = this._findBlockFromId(thread.topBlock, target); - if (eventBlock) { - stack.push(`event ${eventBlock.opcode}`); - } else { - stack.push(`event ${thread.topBlock}`); + return final_traceback.join("\n"); + } + + _jumpToTargetAndBlock(target_id, blockId) { + if (target_id != this.runtime.vm.editingTarget.id) { + this.runtime.vm.setEditingTarget(target_id); + this.runtime.vm.refreshWorkspace(); } - 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;"); + if (!blockId || !this.isScratchBlocksReady) return; + + const workspace = this.ScratchBlocks.getMainWorkspace(); + workspace.centerOnBlock(blockId); } breakpoint() { 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 } ); } }