Skip to content

Commit fa3bc6e

Browse files
committed
improve code generation to be cleaner and add support for basic function inlining
1 parent 4b6d345 commit fa3bc6e

18 files changed

Lines changed: 73 additions & 104 deletions

src/compiler/inputs.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class TypedInput {
6767
asInt () {
6868
if (this.type === TYPES.NUMBER_INT) return this.source;
6969
if (this.type === TYPES.NUMBER) return `(${this.source} | 0)`;
70-
return `toNotNaN(${this.source} | 0)`;
70+
return `(${this.source} | 0)`;
7171
}
7272

7373
asNumberOrNaN () {
@@ -123,9 +123,8 @@ class TypedInput {
123123
}
124124

125125
isAlwaysNumberOrNaN () {
126-
return this.type === TYPES.NUMBER ||
127-
this.type === TYPES.NUMBER_NAN ||
128-
this.type === TYPES.NUMBER_INT;
126+
return this.isAlwaysNumber() ||
127+
this.type === TYPES.NUMBER_NAN;
129128
}
130129

131130
isNeverNumber () {
@@ -385,7 +384,7 @@ class VariableInput {
385384
if (this.type === TYPES.NUMBER_INT) return this.source;
386385
if (this.type === TYPES.NUMBER ||
387386
this.type === TYPES.NUMBER_NAN) return `(${this.source} | 0)`;
388-
return `toNotNaN(+${this.source} | 0)`;
387+
return `(+${this.source} | 0)`;
389388
}
390389

391390
asNumberOrNaN () {

src/compiler/jsgen.js

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -521,11 +521,17 @@ class JSGenerator {
521521
this.usedMathFunctions.add('abs');
522522
return new TypedInput(`abs(${value.asNumber()})`, TYPES.NUMBER);
523523
}
524-
case BLOCKS.OP.ACOS:
524+
case BLOCKS.OP.ACOS: {
525525
// Needs to be marked as NaN because Math.acos(1.0001) === NaN
526+
const value = this.descendInput(node.value);
527+
if (value.isAlwaysConstant()) {
528+
const val = toNotNaN(+value.constantValue);
529+
return new ConstantInput(Math.acos(val) * 180 / Math.PI, false);
530+
}
526531
this.usedMathFunctions.add('acos');
527532
this.usedMathFunctions.add('PI');
528-
return new TypedInput(`((acos(${this.descendInput(node.value).asNumber()}) * 180) / PI)`, TYPES.NUMBER_NAN);
533+
return new TypedInput(`((acos(${value.asNumber()}) * 180) / PI)`, TYPES.NUMBER_NAN);
534+
}
529535
case BLOCKS.OP.ADD: {
530536
// Needs to be marked as NaN because Infinity + -Infinity === NaN
531537
const left = this.descendInput(node.left);
@@ -705,15 +711,15 @@ class JSGenerator {
705711
if (left.isAlwaysConstant() && right.isAlwaysConstant()) {
706712
return new ConstantInput(Cast.compare(left.constantValue, right.constantValue) > 0, false);
707713
}
708-
if (left.isAlwaysFinite() && right.isAlwaysFinite()) {
714+
if (left.isAlwaysNumber() && right.isAlwaysNumberOrNaN()) {
709715
return new TypedInput(`(${left.asNumber()} > ${right.asNumber()})`, TYPES.BOOLEAN);
710716
}
711-
if (left.isAlwaysNumber() && right.isAlwaysNumber()) {
712-
return new TypedInput(`(${left.asNumber()} > ${right.asNumber()})`, TYPES.BOOLEAN);
717+
if (left.isAlwaysNumberOrNaN() && right.isAlwaysNumber()) {
718+
return new TypedInput(`!(${left.asNumber()} <= ${right.asNumber()})`, TYPES.BOOLEAN);
713719
}
714-
// When either operand is known to never be a number, avoid all number parsing.
715-
if (left.isNeverNumber() || right.isNeverNumber()) {
716-
return new TypedInput(`(${left.asLowerString()} > ${right.asLowerString()})`, TYPES.BOOLEAN);
720+
if ((left.isAlwaysNumber() && right.isAlwaysNumber()) ||
721+
(left.isNeverNumber() || right.isNeverNumber())) {
722+
return new TypedInput(`(${left.asNumber()} > ${right.asNumber()})`, TYPES.BOOLEAN);
717723
}
718724
// No compile-time optimizations possible - use fallback method.
719725
return new TypedInput(`compareGreaterThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPES.BOOLEAN);
@@ -742,16 +748,15 @@ class JSGenerator {
742748
if (left.isAlwaysConstant() && right.isAlwaysConstant()) {
743749
return new ConstantInput(Cast.compare(left.constantValue, right.constantValue) < 0, false);
744750
}
745-
746-
if (left.isAlwaysFinite() && right.isAlwaysFinite()) {
751+
if (left.isAlwaysNumber() && right.isAlwaysNumberOrNaN()) {
747752
return new TypedInput(`(${left.asNumber()} < ${right.asNumber()})`, TYPES.BOOLEAN);
748753
}
749-
if (left.isAlwaysNumber() && right.isAlwaysNumber()) {
750-
return new TypedInput(`(${left.asNumber()} < ${right.asNumber()})`, TYPES.BOOLEAN);
754+
if (left.isAlwaysNumberOrNaN() && right.isAlwaysNumber()) {
755+
return new TypedInput(`!(${left.asNumber()} >= ${right.asNumber()})`, TYPES.BOOLEAN);
751756
}
752-
// When either operand is known to never be a number, avoid all number parsing.
753-
if (left.isNeverNumber() || right.isNeverNumber()) {
754-
return new TypedInput(`(${left.asLowerString()} < ${right.asLowerString()})`, TYPES.BOOLEAN);
757+
if ((left.isAlwaysNumber() && right.isAlwaysNumber()) ||
758+
(left.isNeverNumber() || right.isNeverNumber())) {
759+
return new TypedInput(`(${left.asNumber()} < ${right.asNumber()})`, TYPES.BOOLEAN);
755760
}
756761
// No compile-time optimizations possible - use fallback method.
757762
return new TypedInput(`compareLessThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPES.BOOLEAN);
@@ -772,15 +777,27 @@ class JSGenerator {
772777
}
773778
return new TypedInput(`((${string.asString()})[${l}] || "")`, TYPES.STRING);
774779
}
775-
case BLOCKS.OP.LN:
780+
case BLOCKS.OP.LN: {
776781
// Needs to be marked as NaN because Math.log(-1) == NaN
782+
const value = this.descendInput(node.value);
783+
if (value.isAlwaysConstant()) {
784+
const val = toNotNaN(+value.constantValue);
785+
return new ConstantInput(Math.log(val), false);
786+
}
777787
this.usedMathFunctions.add('log');
778-
return new TypedInput(`log(${this.descendInput(node.value).asNumber()})`, TYPES.NUMBER_NAN);
779-
case BLOCKS.OP.LOG:
788+
return new TypedInput(`log(${value.asNumber()})`, TYPES.NUMBER_NAN);
789+
}
790+
case BLOCKS.OP.LOG: {
780791
// Needs to be marked as NaN because Math.log(-1) == NaN
792+
const value = this.descendInput(node.value);
793+
if (value.isAlwaysConstant()) {
794+
const val = toNotNaN(+value.constantValue);
795+
return new ConstantInput(Math.log(val) / Math.LN10, false);
796+
}
781797
this.usedMathFunctions.add('log');
782798
this.usedMathFunctions.add('LN10');
783-
return new TypedInput(`(log(${this.descendInput(node.value).asNumber()}) / LN10)`, TYPES.NUMBER_NAN);
799+
return new TypedInput(`(log(${value.asNumber()}) / LN10)`, TYPES.NUMBER_NAN);
800+
}
784801
case BLOCKS.OP.MOD: {
785802
this.descendedIntoModulo = true;
786803
const left = this.descendInput(node.left);
@@ -877,7 +894,8 @@ class JSGenerator {
877894
const procedureCode = node.code;
878895
const procedureVariant = node.variant;
879896
const procedureData = this.ir.procedures[procedureVariant];
880-
if (procedureData.stack === null) {
897+
const stack = procedureData.stack;
898+
if (stack === null || stack.length === 0) {
881899
// Procedure has no body; still evaluate arguments for side effects
882900
const args = [];
883901
for (const input of node.arguments) {
@@ -889,6 +907,13 @@ class JSGenerator {
889907
return new TypedInput('""', TYPES.STRING);
890908
}
891909

910+
if (node.arguments.length === 0) {
911+
if (stack[0].kind === BLOCKS.PROCEDURES.RETURN) {
912+
const input = this.descendInput(stack[0].value);
913+
return input;
914+
}
915+
}
916+
892917
// Recursion makes this complicated because:
893918
// - We need to yield *between* each call in the same command block
894919
// - We need to evaluate arguments *before* that yield happens
@@ -1475,23 +1500,24 @@ class JSGenerator {
14751500
const procedureCode = node.code;
14761501
const procedureVariant = node.variant;
14771502
const procedureData = this.ir.procedures[procedureVariant];
1478-
if (procedureData.stack === null) {
1503+
const stack = procedureData.stack;
1504+
if (stack === null || stack.length === 0) {
14791505
// Procedure has no body; still evaluate arguments for side effects
14801506
break;
14811507
}
14821508

1483-
if (this._canInlineProcedureCallInStack(node, procedureData)) {
1484-
this._emitInlinedProcedureCallInStack(node, procedureData);
1485-
this.resetVariableInputs();
1486-
this.clearVariableTypes();
1487-
break;
1488-
}
1489-
14901509
const yieldForRecursion = !this.isWarp && procedureCode === this.script.procedureCode;
14911510
if (yieldForRecursion) {
14921511
this.yieldNotWarp();
14931512
}
14941513

1514+
if (node.arguments.length === 0 && stack.length === 1) {
1515+
if ([BLOCKS.VAR.SET].includes(stack[0].kind)) {
1516+
this.descendStack(stack, new Frame(false));
1517+
break;
1518+
}
1519+
}
1520+
14951521
if (procedureData.yields) {
14961522
this.source += 'yield* ';
14971523
}
@@ -1692,7 +1718,9 @@ class JSGenerator {
16921718
stopScript () {
16931719
this._flushMonitorUpdates();
16941720
if (this.isProcedure) {
1695-
this.source += 'return "";\n';
1721+
if (this.script.stack[0]?.kind !== BLOCKS.PROCEDURES.RETURN) {
1722+
this.source += 'return "";\n';
1723+
}
16961724
} else {
16971725
this.retire();
16981726
}

test/snapshot/__snapshots__/mw-get-from-list.sb3.tw-snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ b2.value = (b0.value[((toNotNaN(+b1.value) + 1) | 0) - 1] ?? "");
2222
if (compareEqual(b2.value, "")) {
2323
runtime.ext_scratch3_looks._say("pass", target);
2424
}
25-
thread.procedures["Zdo funny"]();
25+
b1.value = "2";
2626
b2.value = (b0.value[((toNotNaN(+b1.value) - 1) | 0) - 1] ?? "");
2727
if ((("" + b2.value).toLowerCase() === "thing")) {
2828
runtime.ext_scratch3_looks._say("pass", target);

test/snapshot/__snapshots__/tw-NaN.sb3.tw-snapshot

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,13 @@ const PI=Math.PI;
1010
const sin=Math.sin;
1111
const cos=Math.cos;
1212
const asin=Math.asin;
13-
const acos=Math.acos;
1413
const round=Math.round;
15-
const log=Math.log;
16-
const LN10=Math.LN10;
1714
return function* genXYZ () {
1815
runtime.ext_scratch3_looks._say("plan 20", target);
1916
runtime.ext_scratch3_looks._say("pass", target);
2017
runtime.ext_scratch3_looks._say("pass", target);
21-
if ((("" + ((acos(1.01) * 180) / PI)).toLowerCase() === "nan")) {
2218
runtime.ext_scratch3_looks._say("pass", target);
23-
}
24-
if (compareEqual((toNotNaN(((acos(1.01) * 180) / PI)) * 1), "0")) {
2519
runtime.ext_scratch3_looks._say("pass", target);
26-
}
2720
runtime.ext_scratch3_looks._say("pass", target);
2821
runtime.ext_scratch3_looks._say("pass", target);
2922
runtime.ext_scratch3_looks._say("pass", target);
@@ -32,18 +25,10 @@ runtime.ext_scratch3_looks._say("pass", target);
3225
runtime.ext_scratch3_looks._say("pass", target);
3326
runtime.ext_scratch3_looks._say("pass", target);
3427
runtime.ext_scratch3_looks._say("pass", target);
35-
if ((("" + log(-1)).toLowerCase() === "nan")) {
3628
runtime.ext_scratch3_looks._say("pass", target);
37-
}
38-
if (compareEqual((toNotNaN(log(-1)) * 1), "0")) {
3929
runtime.ext_scratch3_looks._say("pass", target);
40-
}
41-
if ((("" + (log(-1) / LN10)).toLowerCase() === "nan")) {
4230
runtime.ext_scratch3_looks._say("pass", target);
43-
}
44-
if (compareEqual((toNotNaN((log(-1) / LN10)) * 1), "0")) {
4531
runtime.ext_scratch3_looks._say("pass", target);
46-
}
4732
if (compareEqual((toNotNaN((round(sin((PI * Infinity) / 180) * 1e10) / 1e10)) * 1), "0")) {
4833
runtime.ext_scratch3_looks._say("pass", target);
4934
}

test/snapshot/__snapshots__/tw-custom-report-repeat.sb3.tw-snapshot

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
1010
return function* genXYZ () {
1111
runtime.ext_scratch3_looks._say("plan 1", target);
1212
b0.value = "0";
13-
for (var a0 = toNotNaN(+thread.procedures["Zblock name"]()); a0 >= 0.5; a0--) {
13+
for (var a0 = 40; a0 >= 0.5; a0--) {
1414
b0.value = (toNotNaN(+b0.value) + 1);
1515
yield;
1616
}
@@ -28,5 +28,4 @@ const runtime = target.runtime;
2828
const stage = runtime.getTargetForStage();
2929
return function funXYZ_block_name () {
3030
return "40";
31-
return "";
3231
}; })

test/snapshot/__snapshots__/tw-forkphorus-515-random-with-invalid-number-with-period.sb3.tw-snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
1010
return function* genXYZ () {
1111
runtime.ext_scratch3_looks._say("plan 6", target);
1212
b0.value = runtime.ext_scratch3_operators._random("an invalid number.", "1");
13-
if (compareGreaterThan(b0.value, "0")) {
13+
if (!(toNotNaN(b0.value) <= 0)) {
1414
runtime.ext_scratch3_looks._say("pass", target);
1515
}
1616
if (compareLessThan(b0.value, "1")) {
@@ -20,7 +20,7 @@ if ((("" + b0.value).toLowerCase().indexOf(".") !== -1)) {
2020
runtime.ext_scratch3_looks._say("pass", target);
2121
}
2222
b0.value = runtime.ext_scratch3_operators._random("1", "an invalid number.");
23-
if (compareGreaterThan(b0.value, "0")) {
23+
if (!(toNotNaN(b0.value) <= 0)) {
2424
runtime.ext_scratch3_looks._say("pass", target);
2525
}
2626
if (compareLessThan(b0.value, "1")) {

test/snapshot/__snapshots__/tw-gh-201-stop-script-does-not-reevaluate-arguments.sb3.tw-snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const runtime = target.runtime;
88
const stage = runtime.getTargetForStage();
99
return function* genXYZ () {
1010
runtime.ext_scratch3_looks._say("plan 0", target);
11-
thread.procedures["Zfoo %s"](thread.procedures["Zno op"]());
11+
thread.procedures["Zfoo %s"]("");
1212
runtime.ext_scratch3_looks._say("end", target);
1313
retire(); return;
1414
}; })

test/snapshot/__snapshots__/tw-procedure-call-resets-variable-input-types-430811055.sb3.tw-snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
1010
return function* genXYZ () {
1111
runtime.ext_scratch3_looks._say("plan 1", target);
1212
b0.value = "";
13-
thread.procedures["Zdo something"]();
13+
b0.value = "help";
1414
if (!compareEqual(b0.value, "")) {
1515
runtime.ext_scratch3_looks._say("pass", target);
1616
}

test/snapshot/__snapshots__/tw-procedure-return-non-existant.sb3.tw-snapshot

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,8 @@ const b0 = stage.variables["Go=PJS7BFXYo_qi2S:kQ"];
3434
return function* genXYZ () {
3535
runtime.ext_scratch3_looks._say("plan 2", target);
3636
b0.value = "0";
37-
if (compareEqual(thread.procedures["Zinvalid params - reporter"](), "0")) {
3837
runtime.ext_scratch3_looks._say("pass invalid params reporter", target);
39-
}
40-
if (compareEqual(thread.procedures["Zinvalid params - boolean"](), "0")) {
4138
runtime.ext_scratch3_looks._say("pass invalid params boolean", target);
42-
}
4339
runtime.stopForTarget(target, thread);
4440
runtime.ext_scratch3_looks._say("end", target);
4541
retire(); return;
@@ -52,7 +48,6 @@ const runtime = target.runtime;
5248
const stage = runtime.getTargetForStage();
5349
return function funXYZ_invalid_params___rep () {
5450
return 0;
55-
return "";
5651
}; })
5752

5853
// Sprite1 Zinvalid params - boolean
@@ -62,5 +57,4 @@ const runtime = target.runtime;
6257
const stage = runtime.getTargetForStage();
6358
return function funXYZ_invalid_params___boo () {
6459
return 0;
65-
return "";
6660
}; })

test/snapshot/__snapshots__/tw-procedure-return-simple.sb3.tw-snapshot

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ const stage = runtime.getTargetForStage();
99
const b0 = stage.variables["`jEk@4|i[#Fk?(8x)AV.-my variable"];
1010
return function* genXYZ () {
1111
runtime.ext_scratch3_looks._say("plan 8", target);
12-
if ((("" + thread.procedures["Zsimplest"]()).toLowerCase() === "it works!")) {
1312
runtime.ext_scratch3_looks._say("pass simplest", target);
14-
}
1513
if ((("" + thread.procedures["Znesting 1"]()).toLowerCase() === "42-54")) {
1614
runtime.ext_scratch3_looks._say("pass nesting1", target);
1715
}
@@ -36,7 +34,6 @@ const runtime = target.runtime;
3634
const stage = runtime.getTargetForStage();
3735
return function funXYZ_simplest () {
3836
return "It works!";
39-
return "";
4037
}; })
4138

4239
// Sprite1 Znesting 1
@@ -57,7 +54,6 @@ const runtime = target.runtime;
5754
const stage = runtime.getTargetForStage();
5855
return function funXYZ_warp_fib_ (p0) {
5956
return thread.procedures["Wfib %s"](p0);
60-
return "";
6157
}; })
6258

6359
// Sprite1 Wfactorial %s
@@ -101,7 +97,6 @@ const runtime = target.runtime;
10197
const stage = runtime.getTargetForStage();
10298
return function funXYZ_nesting_2 () {
10399
return "discard nesting 2";
104-
return "";
105100
}; })
106101

107102
// Sprite1 Znesting 3 %s %s
@@ -111,7 +106,6 @@ const runtime = target.runtime;
111106
const stage = runtime.getTargetForStage();
112107
return function funXYZ_nesting_3__ (p0,p1) {
113108
return (toNotNaN(+p0) * toNotNaN(+p1));
114-
return "";
115109
}; })
116110

117111
// Sprite1 Wfib %s
@@ -135,5 +129,4 @@ const runtime = target.runtime;
135129
const stage = runtime.getTargetForStage();
136130
return function funXYZ_no_shadowing_2__ (p0,p1) {
137131
return "discard shadow 2";
138-
return "";
139132
}; })

0 commit comments

Comments
 (0)