Skip to content

Commit 814b2bd

Browse files
committed
Merge branch 'develop' of https://github.com/TurboWarp/scratch-vm into develop
2 parents 1bb33e6 + 7fefa6f commit 814b2bd

File tree

148 files changed

+3027
-2865
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+3027
-2865
lines changed

src/blocks/scratch3_looks.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ class Scratch3LooksBlocks {
352352
}
353353

354354
think (args, util) {
355-
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, util.target, 'think', args.MESSAGE);
355+
this._think(args.MESSAGE, util.target);
356+
}
357+
_think (message, target) { // used by compiler
358+
this.runtime.emit(Scratch3LooksBlocks.SAY_OR_THINK, target, 'think', message);
356359
}
357360

358361
thinkforsecs (args, util) {

src/compiler/compat-blocks.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010
const stacked = [
1111
'looks_changestretchby',
1212
'looks_hideallsprites',
13-
'looks_say',
1413
'looks_sayforsecs',
1514
'looks_setstretchto',
1615
'looks_switchbackdroptoandwait',
17-
'looks_think',
1816
'looks_thinkforsecs',
1917
'motion_align_scene',
2018
'motion_glidesecstoxy',

src/compiler/enums.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ const StackOpcode = {
161161
LOOKS_BACKDROP_SET: 'looks.switchBackdrop',
162162
LOOKS_COSTUME_NEXT: 'looks.nextCostume',
163163
LOOKS_COSTUME_SET: 'looks.switchCostume',
164+
LOOKS_SAY: 'looks.say',
165+
LOOKS_THINK: 'looks.think',
164166

165167
MOTION_X_SET: 'motion.setX',
166168
MOTION_X_CHANGE: 'motion.changeX',

src/compiler/irgen.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ class ScriptTreeGenerator {
419419
}
420420
return new IntermediateInput(InputOpcode.LOOKS_COSTUME_NAME, InputType.STRING);
421421
case 'looks_size':
422-
return new IntermediateInput(InputOpcode.LOOKS_SIZE_GET, InputType.NUMBER_POS);
422+
return new IntermediateInput(InputOpcode.LOOKS_SIZE_GET, InputType.NUMBER_POS | InputType.NUMBER_ZERO);
423423

424424
case 'motion_direction':
425425
return new IntermediateInput(InputOpcode.MOTION_DIRECTION_GET, InputType.NUMBER_REAL);
@@ -640,6 +640,7 @@ class ScriptTreeGenerator {
640640
}
641641

642642
if (object.isConstant('_stage_')) {
643+
// We assume that the stage always exists, so these don't need to be able to return 0.
643644
switch (property) {
644645
case 'background #': // fallthrough for scratch 1.0 compatibility
645646
case 'backdrop #':
@@ -648,6 +649,7 @@ class ScriptTreeGenerator {
648649
return new IntermediateInput(InputOpcode.SENSING_OF_BACKDROP_NAME, InputType.STRING);
649650
}
650651
} else {
652+
// If the target sprite does not exist, these may all return 0, even the costume name one.
651653
switch (property) {
652654
case 'x position':
653655
return new IntermediateInput(InputOpcode.SENSING_OF_POS_X, InputType.NUMBER, {object});
@@ -656,11 +658,11 @@ class ScriptTreeGenerator {
656658
case 'direction':
657659
return new IntermediateInput(InputOpcode.SENSING_OF_DIRECTION, InputType.NUMBER_REAL, {object});
658660
case 'costume #':
659-
return new IntermediateInput(InputOpcode.SENSING_OF_COSTUME_NUMBER, InputType.NUMBER_POS_INT, {object});
661+
return new IntermediateInput(InputOpcode.SENSING_OF_COSTUME_NUMBER, InputType.NUMBER_POS_INT | InputType.NUMBER_ZERO, {object});
660662
case 'costume name':
661-
return new IntermediateInput(InputOpcode.SENSING_OF_COSTUME_NAME, InputType.STRING, {object});
663+
return new IntermediateInput(InputOpcode.SENSING_OF_COSTUME_NAME, InputType.STRING | InputType.NUMBER_ZERO, {object});
662664
case 'size':
663-
return new IntermediateInput(InputOpcode.SENSING_OF_SIZE, InputType.NUMBER_POS, {object});
665+
return new IntermediateInput(InputOpcode.SENSING_OF_SIZE, InputType.NUMBER_POS | InputType.NUMBER_ZERO, {object});
664666
}
665667
}
666668

@@ -806,7 +808,7 @@ class ScriptTreeGenerator {
806808
// Dirty hack: automatically enable warp timer for this block if it uses timer
807809
// This fixes project that do things like "repeat until timer > 0.5"
808810
this.usesTimer = false;
809-
const condition = this.descendInputOfBlock(block, 'CONDITION');
811+
const condition = this.descendInputOfBlock(block, 'CONDITION').toType(InputType.BOOLEAN);
810812
const needsWarpTimer = this.usesTimer;
811813
return new IntermediateStackBlock(StackOpcode.CONTROL_WHILE, {
812814
condition: new IntermediateInput(InputOpcode.OP_NOT, InputType.BOOLEAN, {
@@ -952,6 +954,10 @@ class ScriptTreeGenerator {
952954
return new IntermediateStackBlock(StackOpcode.LOOKS_BACKDROP_NEXT);
953955
case 'looks_nextcostume':
954956
return new IntermediateStackBlock(StackOpcode.LOOKS_COSTUME_NEXT);
957+
case 'looks_say':
958+
return new IntermediateStackBlock(StackOpcode.LOOKS_SAY, {
959+
message: this.descendInputOfBlock(block, 'MESSAGE')
960+
});
955961
case 'looks_seteffectto':
956962
return new IntermediateStackBlock(StackOpcode.LOOKS_EFFECT_SET, {
957963
effect: block.fields.EFFECT.value.toLowerCase(),
@@ -971,6 +977,10 @@ class ScriptTreeGenerator {
971977
return new IntermediateStackBlock(StackOpcode.LOOKS_COSTUME_SET, {
972978
costume: this.descendInputOfBlock(block, 'COSTUME', true)
973979
});
980+
case 'looks_think':
981+
return new IntermediateStackBlock(StackOpcode.LOOKS_THINK, {
982+
message: this.descendInputOfBlock(block, 'MESSAGE')
983+
});
974984

975985
case 'motion_changexby':
976986
return new IntermediateStackBlock(StackOpcode.MOTION_X_CHANGE, {

src/compiler/iroptimizer.js

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,48 @@ class IROptimizer {
158158
case InputOpcode.ADDON_CALL:
159159
break;
160160

161+
case InputOpcode.CAST_OBJECT: {
162+
const innerType = inputs.target.type;
163+
if (innerType & InputType.OBJECT) return innerType;
164+
return InputType.OBJECT;
165+
}
166+
167+
case InputOpcode.CAST_ARRAY: {
168+
const innerType = inputs.target.type;
169+
if (innerType & InputType.ARRAY) return innerType;
170+
return InputType.ARRAY;
171+
}
172+
173+
case InputOpcode.CAST_BOOLEAN: {
174+
const innerType = inputs.target.type;
175+
if (innerType & InputType.BOOLEAN) return innerType;
176+
return InputType.BOOLEAN;
177+
}
178+
161179
case InputOpcode.CAST_NUMBER: {
162180
const innerType = inputs.target.type;
163181
if (innerType & InputType.NUMBER) return innerType;
164182
return InputType.NUMBER;
165-
} case InputOpcode.CAST_NUMBER_OR_NAN: {
183+
}
184+
185+
case InputOpcode.CAST_NUMBER_INDEX: {
186+
const innerType = inputs.target.type;
187+
if (innerType & InputType.NUMBER_INDEX) return innerType;
188+
return InputType.NUMBER_INDEX;
189+
}
190+
191+
case InputOpcode.CAST_NUMBER_OR_NAN: {
166192
const innerType = inputs.target.type;
167193
if (innerType & InputType.NUMBER_OR_NAN) return innerType;
168194
return InputType.NUMBER_OR_NAN;
169195
}
170196

197+
case InputOpcode.CAST_STRING: {
198+
const innerType = inputs.target.type;
199+
if (innerType & InputType.STRING) return innerType;
200+
return InputType.STRING;
201+
}
202+
171203
case InputOpcode.OP_ADD: {
172204
const leftType = inputs.left.type;
173205
const rightType = inputs.right.type;
@@ -559,9 +591,12 @@ class IROptimizer {
559591
break;
560592
case StackOpcode.CONTROL_WHILE:
561593
case StackOpcode.CONTROL_FOR:
594+
modified = this.analyzeInputs(inputs, state) || modified;
595+
modified = this.analyzeLoopedStack(inputs.do, state, stackBlock, true) || modified;
596+
break;
562597
case StackOpcode.CONTROL_REPEAT:
563598
modified = this.analyzeInputs(inputs, state) || modified;
564-
modified = this.analyzeLoopedStack(inputs.do, state, stackBlock) || modified;
599+
modified = this.analyzeLoopedStack(inputs.do, state, stackBlock, false) || modified;
565600
break;
566601
case StackOpcode.CONTROL_IF_ELSE: {
567602
modified = this.analyzeInputs(inputs, state) || modified;
@@ -642,20 +677,24 @@ class IROptimizer {
642677
* @param {IntermediateStack} stack
643678
* @param {TypeState} state
644679
* @param {IntermediateStackBlock} block
680+
* @param {boolean} willReevaluateInputs
645681
* @returns {boolean}
646682
* @private
647683
*/
648-
analyzeLoopedStack (stack, state, block) {
684+
analyzeLoopedStack (stack, state, block, willReevaluateInputs) {
685+
let modified = false;
686+
649687
if (block.yields && !this.ignoreYields) {
650-
let modified = state.clear();
688+
modified = state.clear();
689+
if (willReevaluateInputs) {
690+
modified = this.analyzeInputs(block.inputs, state) || modified;
691+
}
651692
block.entryState = state.clone();
652693
block.exitState = state.clone();
653-
modified = this.analyzeInputs(block.inputs, state) || modified;
654694
return this.analyzeStack(stack, state) || modified;
655695
}
656696

657697
let iterations = 0;
658-
let modified = false;
659698
let keepLooping;
660699
do {
661700
// If we are stuck in an apparent infinite loop, give up and assume the worst.
@@ -672,7 +711,10 @@ class IROptimizer {
672711
const newState = state.clone();
673712
modified = this.analyzeStack(stack, newState) || modified;
674713
modified = (keepLooping = state.or(newState)) || modified;
675-
modified = this.analyzeInputs(block.inputs, state) || modified;
714+
715+
if (willReevaluateInputs) {
716+
modified = this.analyzeInputs(block.inputs, state) || modified;
717+
}
676718
} while (keepLooping);
677719
block.entryState = state.clone();
678720
return modified;
@@ -693,19 +735,61 @@ class IROptimizer {
693735
}
694736

695737
switch (input.opcode) {
738+
case InputOpcode.CAST_OBJECT: {
739+
const targetType = input.inputs.target.type;
740+
if ((targetType & InputType.OBJECT) === targetType) {
741+
return input.inputs.target;
742+
}
743+
return input;
744+
}
745+
746+
case InputOpcode.CAST_ARRAY: {
747+
const targetType = input.inputs.target.type;
748+
if ((targetType & InputType.ARRAY) === targetType) {
749+
return input.inputs.target;
750+
}
751+
return input;
752+
}
753+
754+
case InputOpcode.CAST_BOOLEAN: {
755+
const targetType = input.inputs.target.type;
756+
if ((targetType & InputType.BOOLEAN) === targetType) {
757+
return input.inputs.target;
758+
}
759+
return input;
760+
}
761+
696762
case InputOpcode.CAST_NUMBER: {
697763
const targetType = input.inputs.target.type;
698764
if ((targetType & InputType.NUMBER) === targetType) {
699765
return input.inputs.target;
700766
}
701767
return input;
702-
} case InputOpcode.CAST_NUMBER_OR_NAN: {
768+
}
769+
770+
case InputOpcode.CAST_NUMBER_INDEX: {
771+
const targetType = input.inputs.target.type;
772+
if ((targetType & InputType.NUMBER_INDEX) === targetType) {
773+
return input.inputs.target;
774+
}
775+
return input;
776+
}
777+
778+
case InputOpcode.CAST_NUMBER_OR_NAN: {
703779
const targetType = input.inputs.target.type;
704780
if ((targetType & InputType.NUMBER_OR_NAN) === targetType) {
705781
return input.inputs.target;
706782
}
707783
return input;
708784
}
785+
786+
case InputOpcode.CAST_STRING: {
787+
const targetType = input.inputs.target.type;
788+
if ((targetType & InputType.STRING) === targetType) {
789+
return input.inputs.target;
790+
}
791+
return input;
792+
}
709793
}
710794

711795
return input;

src/compiler/jsexecute.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,15 @@ runtimeFunctions.retire = `const retire = () => {
286286
thread.target.runtime.sequencer.retireThread(thread);
287287
}`;
288288

289+
/**
290+
* Converts NaN to zero. Used to match Scratch's string-to-number.
291+
* Unlike (x || 0), -0 stays as -0 and is not converted to 0.
292+
* This function needs to be written such that it's very easy for browsers to inline it.
293+
* @param {number} value A number. Might be NaN.
294+
* @returns {number} A number. Never NaN.
295+
*/
296+
runtimeFunctions.toNotNaN = `const toNotNaN = value => Number.isNaN(value) ? 0 : value`;
297+
289298
/**
290299
* Scratch cast to string.
291300
* Similar to Cast.toString()
@@ -394,12 +403,12 @@ baseRuntime += `const isNotActuallyZero = val => {
394403
*/
395404
baseRuntime += `const compareEqualSlow = (v1, v2) => {
396405
const n1 = +v1;
397-
if (isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
406+
if (Number.isNaN(n1) || (n1 === 0 && isNotActuallyZero(v1))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
398407
const n2 = +v2;
399-
if (isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
408+
if (Number.isNaN(n2) || (n2 === 0 && isNotActuallyZero(v2))) return ('' + v1).toLowerCase() === ('' + v2).toLowerCase();
400409
return n1 === n2;
401410
};
402-
const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) && !isNaN(v2) || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`;
411+
const compareEqual = (v1, v2) => (typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v1) && !Number.isNaN(v2) || v1 === v2) ? v1 === v2 : compareEqualSlow(v1, v2);`;
403412

404413
/**
405414
* Determine if one value is greater than another.
@@ -415,14 +424,14 @@ runtimeFunctions.compareGreaterThan = `const compareGreaterThanSlow = (v1, v2) =
415424
} else if (n2 === 0 && isNotActuallyZero(v2)) {
416425
n2 = NaN;
417426
}
418-
if (isNaN(n1) || isNaN(n2)) {
427+
if (Number.isNaN(n1) || Number.isNaN(n2)) {
419428
const s1 = ('' + v1).toLowerCase();
420429
const s2 = ('' + v2).toLowerCase();
421430
return s1 > s2;
422431
}
423432
return n1 > n2;
424433
};
425-
const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v1) ? v1 > v2 : compareGreaterThanSlow(v1, v2)`;
434+
const compareGreaterThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v1) ? v1 > v2 : compareGreaterThanSlow(v1, v2)`;
426435

427436
/**
428437
* Determine if one value is less than another.
@@ -438,14 +447,14 @@ runtimeFunctions.compareLessThan = `const compareLessThanSlow = (v1, v2) => {
438447
} else if (n2 === 0 && isNotActuallyZero(v2)) {
439448
n2 = NaN;
440449
}
441-
if (isNaN(n1) || isNaN(n2)) {
450+
if (Number.isNaN(n1) || Number.isNaN(n2)) {
442451
const s1 = ('' + v1).toLowerCase();
443452
const s2 = ('' + v2).toLowerCase();
444453
return s1 < s2;
445454
}
446455
return n1 < n2;
447456
};
448-
const compareLessThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !isNaN(v2) ? v1 < v2 : compareLessThanSlow(v1, v2)`;
457+
const compareLessThan = (v1, v2) => typeof v1 === 'number' && typeof v2 === 'number' && !Number.isNaN(v2) ? v1 < v2 : compareLessThanSlow(v1, v2)`;
449458

450459
/**
451460
* Generate a random integer.

src/compiler/jsgen.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ class JSGenerator {
191191
return `(+${this.descendInput(node.target.toType(InputType.BOOLEAN))})`;
192192
}
193193
if (node.target.isAlwaysType(InputType.NUMBER_OR_NAN)) {
194-
return `(${this.descendInput(node.target)} || 0)`;
194+
return `toNotNaN(${this.descendInput(node.target)})`;
195195
}
196-
return `(+${this.descendInput(node.target)} || 0)`;
196+
return `toNotNaN(+${this.descendInput(node.target)})`;
197197
case InputOpcode.CAST_NUMBER_OR_NAN:
198198
return `(+${this.descendInput(node.target)})`;
199199
case InputOpcode.CAST_NUMBER_INDEX:
@@ -350,6 +350,10 @@ class JSGenerator {
350350
const left = node.left;
351351
const right = node.right;
352352

353+
// When either operand is known to never be a number, only use string comparison to avoid all number parsing.
354+
if (!left.isSometimesType(InputType.NUMBER_INTERPRETABLE) || !right.isSometimesType(InputType.NUMBER_INTERPRETABLE)) {
355+
return `(${this.descendInput(left.toType(InputType.STRING))}.toLowerCase() === ${this.descendInput(right.toType(InputType.STRING))}.toLowerCase())`;
356+
}
353357
// When both operands are known to be numbers, we can use ===
354358
if (left.isAlwaysType(InputType.NUMBER_INTERPRETABLE) && right.isAlwaysType(InputType.NUMBER_INTERPRETABLE)) {
355359
return `(${this.descendInput(left.toType(InputType.NUMBER))} === ${this.descendInput(right.toType(InputType.NUMBER))})`;
@@ -358,10 +362,6 @@ class JSGenerator {
358362
if (isSafeInputForEqualsOptimization(left, right) || isSafeInputForEqualsOptimization(right, left)) {
359363
return `(${this.descendInput(left.toType(InputType.NUMBER))} === ${this.descendInput(right.toType(InputType.NUMBER))})`;
360364
}
361-
// When either operand is known to never be a number, only use string comparison to avoid all number parsing.
362-
if (!left.isSometimesType(InputType.NUMBER_INTERPRETABLE) || !right.isSometimesType(InputType.NUMBER_INTERPRETABLE)) {
363-
return `(${this.descendInput(left.toType(InputType.STRING))}.toLowerCase() === ${this.descendInput(right.toType(InputType.STRING))}.toLowerCase())`;
364-
}
365365
// No compile-time optimizations possible - use fallback method.
366366
return `compareEqual(${this.descendInput(left)}, ${this.descendInput(right)})`;
367367
}
@@ -841,6 +841,12 @@ class JSGenerator {
841841
case StackOpcode.LOOKS_COSTUME_SET:
842842
this.source += `runtime.ext_scratch3_looks._setCostume(target, ${this.descendInput(node.costume)});\n`;
843843
break;
844+
case StackOpcode.LOOKS_SAY:
845+
this.source += `runtime.ext_scratch3_looks._say(${this.descendInput(node.message)}, target);\n`;
846+
break;
847+
case StackOpcode.LOOKS_THINK:
848+
this.source += `runtime.ext_scratch3_looks._think(${this.descendInput(node.message)}, target);\n`;
849+
break;
844850

845851
case StackOpcode.MOTION_X_CHANGE:
846852
this.source += `target.setXY(target.x + ${this.descendInput(node.dx)}, target.y);\n`;

0 commit comments

Comments
 (0)