Skip to content
20 changes: 10 additions & 10 deletions src/compiler/jsexecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}`;

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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);
}`;

Expand Down
5 changes: 4 additions & 1 deletion src/engine/block-utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 13 additions & 4 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -2457,7 +2458,7 @@ class Runtime extends EventEmitter {
thread.tryCompile();
}

this.emit(Runtime.THREAD_STARTED, thread);
this.emit(Runtime.THREAD_STARTED, thread, opts);
return thread;
}

Expand Down Expand Up @@ -2625,15 +2626,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.<Thread>} 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];

Expand Down Expand Up @@ -2688,7 +2691,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
Expand All @@ -2707,8 +2712,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;
}

Expand Down
12 changes: 9 additions & 3 deletions src/engine/thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down
94 changes: 67 additions & 27 deletions src/extensions/jg_debugging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ class jgDebuggingBlocks {
this._logs = [];
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", () => {
if (this.isScratchBlocksReady) return;
this.isScratchBlocksReady = typeof ScratchBlocks === "object";
if (!this.isScratchBlocksReady) return;
this.ScratchBlocks = ScratchBlocks;
});
}

/**
Expand Down Expand Up @@ -229,7 +246,7 @@ class jgDebuggingBlocks {
{
opcode: 'breakpoint',
blockType: BlockType.COMMAND,
}
},
]
};
}
Expand All @@ -241,7 +258,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) {
Expand Down Expand Up @@ -368,42 +385,65 @@ 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 commandBlockId = thread.peekStack();
const block = this._findBlockFromId(commandBlockId, target);
if (block) {
stack.push(`block ${block.opcode}`);
} else {
stack.push(`block ${commandBlockId}`);
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` +
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 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, block_id) {
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 (!block_id || !this.isScratchBlocksReady) return;

const workspace = this.ScratchBlocks.getMainWorkspace();
workspace.centerOnBlock(block_id);
}

breakpoint() {
Expand Down