Skip to content

Commit

Permalink
Merge pull request #159 from TurboWarp/more-branching
Browse files Browse the repository at this point in the history
More CONDITIONAL and LOOP stuff
  • Loading branch information
GarboMuffin authored Aug 23, 2023
2 parents 1cb2549 + 4845bf8 commit da3ac70
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 115 deletions.
8 changes: 5 additions & 3 deletions src/compiler/compat-block-utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ class CompatibilityLayerBlockUtility extends BlockUtility {
constructor () {
super();
this._stackFrame = {};
this._startedBranch = null;
}

get stackFrame () {
return this._stackFrame;
}

// Branching operations are not supported.
startBranch () {
throw new Error('startBranch is not supported by this BlockUtility');
startBranch (branchNumber, isLoop) {
this._startedBranch = [branchNumber, isLoop];
}

startProcedure () {
throw new Error('startProcedure is not supported by this BlockUtility');
}
Expand All @@ -33,6 +34,7 @@ class CompatibilityLayerBlockUtility extends BlockUtility {
this.thread = thread;
this.sequencer = thread.target.runtime.sequencer;
this._stackFrame = stackFrame;
this._startedBranch = null;
thread.stack[0] = fakeBlockId;
}
}
Expand Down
46 changes: 27 additions & 19 deletions src/compiler/jsexecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ runtimeFunctions.waitThreads = `const waitThreads = function*(threads) {
* @param {function} blockFunction The primitive's function.
* @param {boolean} useFlags Whether to set flags (hasResumedFromPromise)
* @param {string} blockId Block ID to set on the emulated block utility.
* @param {*|null} stackFrame Object to use as stack frame.
* @param {*|null} branchInfo Extra information object for CONDITIONAL and LOOP blocks. See createBranchInfo().
* @returns {*} the value returned by the block, if any.
*/
runtimeFunctions.executeInCompatibilityLayer = `let hasResumedFromPromise = false;
Expand Down Expand Up @@ -132,24 +132,31 @@ const isPromise = value => (
typeof value === 'object' &&
typeof value.then === 'function'
);
const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, useFlags, blockId, stackFrame) {
const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, useFlags, blockId, branchInfo) {
const thread = globalState.thread;
const blockUtility = globalState.blockUtility;
if (!stackFrame) stackFrame = {};
const stackFrame = branchInfo ? branchInfo.stackFrame : {};
const finish = (returnValue) => {
if (branchInfo) {
if (typeof returnValue === 'undefined' && blockUtility._startedBranch) {
branchInfo.isLoop = blockUtility._startedBranch[1];
return blockUtility._startedBranch[0];
}
branchInfo.isLoop = branchInfo.defaultIsLoop;
return returnValue;
}
return returnValue;
};
const executeBlock = () => {
blockUtility.init(thread, blockId, stackFrame);
return blockFunction(inputs, blockUtility);
};
let returnValue = executeBlock();
if (isPromise(returnValue)) {
returnValue = yield* waitPromise(returnValue);
if (useFlags) {
hasResumedFromPromise = true;
}
return returnValue;
return finish(yield* waitPromise(returnValue));
}
if (thread.status === 1 /* STATUS_PROMISE_WAIT */) {
Expand All @@ -173,30 +180,31 @@ const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, use
}
returnValue = executeBlock();
if (isPromise(returnValue)) {
returnValue = yield* waitPromise(returnValue);
if (useFlags) {
hasResumedFromPromise = true;
}
return returnValue;
return finish(yield* waitPromise(returnValue));
}
if (thread.status === 1 /* STATUS_PROMISE_WAIT */) {
yield;
return '';
return finish('');
}
}
// todo: do we have to do anything extra if status is STATUS_DONE?
return returnValue;
return finish(returnValue);
}`;

/**
* @returns {unknown} An object to use as a stack frame.
* @param {boolean} isLoop True if the block is a LOOP by default (can be overridden by startBranch() call)
* @returns {unknown} Branch info object for compatibility layer.
*/
runtimeFunctions.persistentStackFrame = `const persistentStackFrame = () => ({});`;
runtimeFunctions.createBranchInfo = `const createBranchInfo = (isLoop) => ({
defaultIsLoop: isLoop,
isLoop: false,
branch: 0,
stackFrame: {}
});`;

/**
* End the current script.
Expand Down
22 changes: 12 additions & 10 deletions src/compiler/jsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,21 +765,23 @@ class JSGenerator {
const blockType = node.blockType;
if (blockType === BlockType.COMMAND || blockType === BlockType.HAT) {
this.source += `${this.generateCompatibilityLayerCall(node, isLastInLoop)};\n`;
} else if (blockType === BlockType.CONDITIONAL) {
this.source += `switch (Math.round(${this.generateCompatibilityLayerCall(node, isLastInLoop)})) {\n`;
} else if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) {
const branchVariable = this.localVariables.next();
this.source += `const ${branchVariable} = createBranchInfo(${blockType === BlockType.LOOP});\n`;
this.source += `while (${branchVariable}.branch = +(${this.generateCompatibilityLayerCall(node, false, branchVariable)})) {\n`;
this.source += `switch (${branchVariable}.branch) {\n`;
for (let i = 0; i < node.substacks.length; i++) {
this.source += `case ${i + 1}: {\n`;
this.descendStack(node.substacks[i], new Frame(false));
this.source += `break;\n`;
this.source += `}\n`;
this.source += `}\n`; // close case
}
this.source += `}\n`;
} else if (node.blockType === BlockType.LOOP) {
const stackFrameName = this.localVariables.next();
this.source += `const ${stackFrameName} = persistentStackFrame();\n`;
this.source += `while (toBoolean(${this.generateCompatibilityLayerCall(node, isLastInLoop, stackFrameName)})) {\n`;
this.descendStack(node.substacks[0], new Frame(true));
this.source += '}\n';
this.source += '}\n'; // close switch
this.source += `if (!${branchVariable}.isLoop) break;\n`;
this.yieldLoop();
this.source += '}\n'; // close while
} else {
throw new Error(`Unknown block type: ${blockType}`);
}

if (isLastInLoop) {
Expand Down
7 changes: 2 additions & 5 deletions src/engine/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,8 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la
// Predicate returned false: do not allow script to run
sequencer.retireThread(thread);
}
} else if (isConditional) {
const branch = Math.round(resolvedValue);
sequencer.stepToBranch(thread, branch, false);
} else if (isLoop && cast.toBoolean(resolvedValue)) {
sequencer.stepToBranch(thread, 1, true);
} else if ((isConditional || isLoop) && typeof resolvedValue !== 'undefined') {
sequencer.stepToBranch(thread, cast.toNumber(resolvedValue), isLoop);
} else {
// In a non-hat, report the value visually if necessary if
// at the top of the thread stack.
Expand Down
12 changes: 7 additions & 5 deletions src/extension-support/extension-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const SecurityManager = require('./tw-security-manager');
// TODO: move these out into a separate repository?
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods

const builtinExtensions = {
const defaultBuiltinExtensions = {
// This is an example that isn't loaded with the other core blocks,
// but serves as a reference for loading core blocks as extensions.
coreExample: () => require('../blocks/scratch3_core_example'),
Expand Down Expand Up @@ -125,6 +125,8 @@ class ExtensionManager {
this.loadingAsyncExtensions = 0;
this.asyncExtensionsLoadedCallbacks = [];

this.builtinExtensions = Object.assign({}, defaultBuiltinExtensions);

dispatch.setService('extensions', createExtensionService(this)).catch(e => {
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
});
Expand All @@ -148,7 +150,7 @@ class ExtensionManager {
* @returns {boolean}
*/
isBuiltinExtension (extensionId) {
return Object.prototype.hasOwnProperty.call(builtinExtensions, extensionId);
return Object.prototype.hasOwnProperty.call(this.builtinExtensions, extensionId);
}

/**
Expand All @@ -169,15 +171,15 @@ class ExtensionManager {
return;
}

const extension = builtinExtensions[extensionId]();
const extension = this.builtinExtensions[extensionId]();
const extensionInstance = new extension(this.runtime);
const serviceName = this._registerInternalExtension(extensionInstance);
this._loadedExtensions.set(extensionId, serviceName);
this.runtime.compilerRegisterExtension(extensionId, extensionInstance);
}

addBuiltinExtension (extensionId, extensionClass) {
builtinExtensions[extensionId] = () => extensionClass;
this.builtinExtensions[extensionId] = () => extensionClass;
}

_isValidExtensionURL (extensionURL) {
Expand Down Expand Up @@ -593,7 +595,7 @@ class ExtensionManager {
getExtensionURLs () {
const extensionURLs = {};
for (const [extensionId, serviceName] of this._loadedExtensions.entries()) {
if (builtinExtensions.hasOwnProperty(extensionId)) {
if (this.builtinExtensions.hasOwnProperty(extensionId)) {
continue;
}

Expand Down
Binary file modified test/fixtures/tw-conditional.sb3
Binary file not shown.
Binary file modified test/fixtures/tw-loop.sb3
Binary file not shown.
12 changes: 12 additions & 0 deletions test/integration/tw_add_builtin_extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,15 @@ test('addBuiltingExtension', t => {
vm.runtime._step();
});
});

test('each runtime has own set of extensions', t => {
const vm1 = new VM();
const vm2 = new VM();

vm1.extensionManager.addBuiltinExtension('testbuiltin', TestBuiltinExtension)

t.ok(vm1.extensionManager.isBuiltinExtension('testbuiltin'));
t.notOk(vm2.extensionManager.isBuiltinExtension('testbuiltin'));

t.end();
});
Loading

0 comments on commit da3ac70

Please sign in to comment.