diff --git a/HISTORY.md b/HISTORY.md
index db7e7e1f..86d269cb 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -3,11 +3,15 @@
## in development:
* **Notable Changes:**
+ * optimized special cases for COMBINE (sum, product, min, max) by up to 34 x
* custom block label parts inside the prototype (in the block editor) are now displayed the same as in block instances
* variadic ring inputs are now arranged vertically (e.g. the reporter rings in PIPE)
* changed zebra-coloring for yellow custom block prototypes (in the block editor) so the hat block changes the shade, not the prototype
* improved layout and rendering of (+) buttons in custom block prototypes
+### 2021-03-02
+* threads: optimized special cases for COMBINE (sum, product, min, max) by up to 34 x
+
### 2021-03-01
* byob: improved layout and rendering of (+) buttons in custom block prototypes
* byob: display custom block label parts in the prototype (in the block editor) the same as in block instances
diff --git a/snap.html b/snap.html
index 50f8952a..f77cb65a 100755
--- a/snap.html
+++ b/snap.html
@@ -9,7 +9,7 @@
-
+
diff --git a/src/threads.js b/src/threads.js
index a06ac827..0dfd0ffa 100644
--- a/src/threads.js
+++ b/src/threads.js
@@ -61,7 +61,7 @@ StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy, Map,
isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph, BLACK,
TableFrameMorph, ColorSlotMorph, isSnapObject, newCanvas, Symbol, SVG_Costume*/
-modules.threads = '2021-February-23';
+modules.threads = '2021-March-02';
var ThreadManager;
var Process;
@@ -2899,6 +2899,14 @@ Process.prototype.reportCombine = function (list, reporter) {
}
if (list.isLinked) {
if (this.context.accumulator === null) {
+ // check for special cases to speed up
+ if (this.canRunOptimizedForCombine(reporter)) {
+ return this.reportListAggregation(
+ list,
+ reporter.expression.selector
+ );
+ }
+ // initialize the accumulator
this.context.accumulator = {
source : list.cdr(),
idx : 1,
@@ -2919,6 +2927,14 @@ Process.prototype.reportCombine = function (list, reporter) {
next = this.context.accumulator.source.at(1);
} else { // arrayed
if (this.context.accumulator === null) {
+ // check for special cases to speed up
+ if (this.canRunOptimizedForCombine(reporter)) {
+ return this.reportListAggregation(
+ list,
+ reporter.expression.selector
+ );
+ }
+ // initialize the accumulator
this.context.accumulator = {
idx : 1,
target : list.at(1)
@@ -2946,6 +2962,54 @@ Process.prototype.reportCombine = function (list, reporter) {
this.evaluate(reporter, new List(parms));
};
+Process.prototype.reportListAggregation = function (list, selector) {
+ // private - used by reportCombine to optimize certain commutative
+ // operations such as sum, product, min, max hyperized all at once
+ var len = list.length(),
+ result, i;
+
+ if (len === 0) {
+ return 0;
+ }
+ result = list.at(1);
+ if (len > 1) {
+ for (i = 2; i <= len; i += 1) {
+ result = this[selector](result, list.at(i));
+ }
+ }
+ return result;
+};
+
+Process.prototype.canRunOptimizedForCombine = function (aContext) {
+ // private - used by reportCombine to check for optimizable
+ // special cases
+ var op = aContext.expression.selector,
+ eligible;
+ if (!op) {
+ return false;
+ }
+ eligible = ['reportSum', 'reportProduct', 'reportMin', 'reportMax'];
+ if (!contains(eligible, op)) {
+ return false;
+ }
+
+ // scan the expression's inputs, we can assume there are exactly two,
+ // because we're only looking at eligible selectors. Make sure none is
+ // a non-empty input slot or a variable getter whose name doesn't
+ // correspond to an input of the context.
+ // make sure the context has either no or exactly two inputs.
+ if (aContext.inputs.length === 0) {
+ return aContext.expression.inputs().every(each => each.bindingID);
+ }
+ if (aContext.inputs.length !== 2) {
+ return false;
+ }
+ return aContext.expression.inputs().every(each =>
+ each.selector === 'reportGetVar' &&
+ contains(aContext.inputs, each.blockSpec)
+ );
+};
+
// Process interpolated primitives
Process.prototype.doWait = function (secs) {