Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1600378
Starting functionality for a better traceback
Steve0Greatness Aug 6, 2025
e75feee
Update JSExecute definition of startHats
Steve0Greatness Aug 7, 2025
4dd9848
Merge branch 'develop' of https://github.com/penguinmod/penguinmod-vm…
Steve0Greatness Aug 7, 2025
fa41c30
Only store string indexes, fix clicking on a script
Steve0Greatness Aug 7, 2025
dd15f5b
Start working on block jumping + render traceback as text
Steve0Greatness Aug 7, 2025
5f3780f
Fix jumping to block (partly)
Steve0Greatness Aug 7, 2025
a9b1a83
Replace function here with the built-in ScratchBlocks workspace method
Steve0Greatness Aug 29, 2025
8db4dd2
Move traceback handling internal
Steve0Greatness Aug 29, 2025
a71cc09
Move traceback handling back into jg_debugging
Steve0Greatness Aug 29, 2025
d75798f
Merge branch 'develop' into debugging-patch01
Steve0Greatness Nov 27, 2025
7f4edbf
JSGen shenanigans to get procedure tracing working
Steve0Greatness Nov 28, 2025
e7be01e
Add link to jump to blocks in project
Steve0Greatness Nov 28, 2025
ecd58b6
Use camel case on the parent-thread for the hats started event
Steve0Greatness Dec 8, 2025
43c5bc4
Camel case the block id in the traceback
Steve0Greatness Dec 8, 2025
ea25c6c
Don't render in HTML for the console print
Steve0Greatness Dec 8, 2025
1b771e6
Add traceback to warning
Steve0Greatness Dec 8, 2025
80771f9
Replace block name with procCode for stack elements that represent proc
Steve0Greatness Dec 8, 2025
72fd48e
Allow tracing with scripts (ext) and new thread (block).
Steve0Greatness Dec 8, 2025
2a298ba
Use a local variable name, here.
Steve0Greatness Dec 8, 2025
4d52e49
Add padding to debugging window
Steve0Greatness Dec 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 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,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];
}`;

Expand All @@ -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);
}`;

Expand Down
5 changes: 4 additions & 1 deletion src/compiler/jsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
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 @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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.<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 @@ -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
Expand All @@ -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;
}

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
132 changes: 101 additions & 31 deletions src/extensions/jg_debugging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;';

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

/**
Expand Down Expand Up @@ -229,7 +262,7 @@ class jgDebuggingBlocks {
{
opcode: 'breakpoint',
blockType: BlockType.COMMAND,
}
},
]
};
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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 :
`<a
style="color:${linkColor}"
href="javascript:vm.runtime.ext_jgDebugging._jumpToTargetAndBlock('${target_id}', '${blockId}')"
>${block_name}</a>`;

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() {
Expand Down
15 changes: 13 additions & 2 deletions src/extensions/jg_scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading