Skip to content

Commit 108bdca

Browse files
committed
constant folding
1 parent 598218d commit 108bdca

File tree

2 files changed

+197
-46
lines changed

2 files changed

+197
-46
lines changed

src/compiler/jsexecute.js

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,23 @@ const listIndex = (index, length) => {
426426
* @returns {*} The list item, otherwise empty string if it does not exist.
427427
*/
428428
runtimeFunctions.listGet = `const listGet = (list, idx) => {
429-
const index = listIndex(idx, list.length);
430-
return index === -1 ? '' : list[index];
429+
const len = list.length;
430+
if (typeof idx === 'number') {
431+
idx = idx | 0; // fast int
432+
if (idx < 1 || idx > len) return '';
433+
return list[idx - 1];
434+
}
435+
if (idx === 'last') {
436+
return len === 0 ? '' : list[len - 1];
437+
}
438+
if (idx === 'random' || idx === 'any') {
439+
if (len === 0) return '';
440+
return list[(Math.random() * len) | 0];
441+
}
442+
// Fallback slow path (string/other -> number coercion & bounds)
443+
idx = (+idx || 0) | 0;
444+
if (idx < 1 || idx > len) return '';
445+
return list[idx - 1];
431446
}`;
432447

433448
/**
@@ -438,11 +453,25 @@ runtimeFunctions.listGet = `const listGet = (list, idx) => {
438453
*/
439454
runtimeFunctions.listReplace = `const listReplace = (list, idx, value) => {
440455
const values = list.value;
441-
const index = listIndex(idx, values.length);
442-
if (index !== -1) {
443-
values[index] = value;
444-
list._monitorUpToDate = false;
456+
let len = values.length;
457+
let index;
458+
if (typeof idx === 'number') {
459+
idx = idx | 0;
460+
if (idx < 1 || idx > len) return; // invalid
461+
index = idx - 1;
462+
} else if (idx === 'last') {
463+
if (len === 0) return;
464+
index = len - 1;
465+
} else if (idx === 'random' || idx === 'any') {
466+
if (len === 0) return;
467+
index = (Math.random() * len) | 0;
468+
} else {
469+
idx = (+idx || 0) | 0;
470+
if (idx < 1 || idx > len) return;
471+
index = idx - 1;
445472
}
473+
values[index] = value;
474+
list._monitorUpToDate = false;
446475
};`;
447476

448477
/**
@@ -453,11 +482,32 @@ runtimeFunctions.listReplace = `const listReplace = (list, idx, value) => {
453482
*/
454483
runtimeFunctions.listInsert = `const listInsert = (list, idx, value) => {
455484
const values = list.value;
456-
const index = listIndex(idx, values.length + 1);
457-
if (index !== -1) {
485+
let len = values.length;
486+
let index;
487+
if (typeof idx === 'number') {
488+
idx = idx | 0;
489+
if (idx < 1 || idx > len + 1) return; // invalid
490+
index = idx - 1;
491+
} else if (idx === 'last') {
492+
// Insert at end
493+
index = len;
494+
} else if (idx === 'random' || idx === 'any') {
495+
// Insert at random position including end
496+
index = len === 0 ? 0 : (Math.random() * (len + 1)) | 0;
497+
} else {
498+
idx = (+idx || 0) | 0;
499+
if (idx < 1 || idx > len + 1) return;
500+
index = idx - 1;
501+
}
502+
// Fast paths
503+
if (index === 0) {
504+
values.unshift(value);
505+
} else if (index === len) {
506+
values.push(value);
507+
} else {
458508
values.splice(index, 0, value);
459-
list._monitorUpToDate = false;
460509
}
510+
list._monitorUpToDate = false;
461511
};`;
462512

463513
/**
@@ -467,16 +517,38 @@ runtimeFunctions.listInsert = `const listInsert = (list, idx, value) => {
467517
*/
468518
runtimeFunctions.listDelete = `const listDelete = (list, idx) => {
469519
const values = list.value;
520+
let len = values.length;
470521
if (idx === 'all') {
471-
list.value = [];
472-
list._monitorUpToDate = false;
522+
if (len) {
523+
list.value = [];
524+
list._monitorUpToDate = false;
525+
}
473526
return;
474527
}
475-
const index = listIndex(idx, values.length);
476-
if (index !== -1) {
528+
let index;
529+
if (typeof idx === 'number') {
530+
idx = idx | 0;
531+
if (idx < 1 || idx > len) return;
532+
index = idx - 1;
533+
} else if (idx === 'last') {
534+
if (!len) return;
535+
index = len - 1;
536+
} else if (idx === 'random' || idx === 'any') {
537+
if (!len) return;
538+
index = (Math.random() * len) | 0;
539+
} else {
540+
idx = (+idx || 0) | 0;
541+
if (idx < 1 || idx > len) return;
542+
index = idx - 1;
543+
}
544+
if (index === 0) {
545+
values.shift();
546+
} else if (index === len - 1) {
547+
values.pop();
548+
} else {
477549
values.splice(index, 1);
478-
list._monitorUpToDate = false;
479550
}
551+
list._monitorUpToDate = false;
480552
};`;
481553

482554
/**
@@ -487,15 +559,19 @@ runtimeFunctions.listDelete = `const listDelete = (list, idx) => {
487559
*/
488560
runtimeFunctions.listContains = `const listContains = (list, item) => {
489561
const caseSensitive = vm.runtime.runtimeOptions.caseSensitiveLists || false;
562+
const values = list.value;
490563
if (caseSensitive) {
491-
return list.value.indexOf(item) !== -1;
492-
} else {
493-
for (let i = 0, len = list.value.length; i < len; i++) {
494-
const cur = list.value[i];
495-
if (cur === item || compareEqual(cur, item)) return true;
496-
}
497-
return false;
564+
return values.indexOf(item) !== -1;
565+
}
566+
// Non case-sensitive: attempt ultra-fast primitive checks first.
567+
// Hoist length & avoid repeated property lookups.
568+
for (let i = 0, len = values.length; i < len; i++) {
569+
const cur = values[i];
570+
if (cur === item) return true; // exact equal (covers numbers & same ref)
571+
// Fallback to Scratch semantics compare if types differ or strings with casing differences.
572+
if (compareEqual(cur, item)) return true;
498573
}
574+
return false;
499575
};`;
500576

501577
/**
@@ -506,16 +582,17 @@ runtimeFunctions.listContains = `const listContains = (list, item) => {
506582
*/
507583
runtimeFunctions.listIndexOf = `const listIndexOf = (list, item) => {
508584
const caseSensitive = vm.runtime.runtimeOptions.caseSensitiveLists || false;
585+
const values = list.value;
509586
if (caseSensitive) {
510-
const index = list.value.indexOf(item) + 1;
587+
const index = values.indexOf(item) + 1;
511588
return index > 0 ? index : 0;
512-
} else {
513-
for (let i = 0, len = list.value.length; i < len; i++) {
514-
const cur = list.value[i];
515-
if (cur === item || compareEqual(cur, item)) return i + 1;
516-
}
517-
return 0;
518589
}
590+
for (let i = 0, len = values.length; i < len; i++) {
591+
const cur = values[i];
592+
if (cur === item) return i + 1;
593+
if (compareEqual(cur, item)) return i + 1;
594+
}
595+
return 0;
519596
};`;
520597

521598
/**

src/compiler/jsgen.js

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ class TypedInput {
8181
return `(+${this.source} || 0)`;
8282
}
8383

84+
asInt () {
85+
if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return `(${this.source} | 0)`;
86+
return `(+${this.source} | 0)`;
87+
}
88+
8489
asNumberOrNaN () {
8590
if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return this.source;
8691
return `(+${this.source})`;
@@ -150,6 +155,17 @@ class ConstantInput {
150155
return '0';
151156
}
152157

158+
asInt () {
159+
const numberValue = +this.constantValue;
160+
if (numberValue) {
161+
return (numberValue | 0).toString();
162+
}
163+
if (Object.is(numberValue, -0)) {
164+
return '-0';
165+
}
166+
return '0';
167+
}
168+
153169
asNumberOrNaN () {
154170
return this.asNumber();
155171
}
@@ -261,6 +277,12 @@ class VariableInput {
261277
return `(+${this.source} || 0)`;
262278
}
263279

280+
asInt () {
281+
if (this.type === TYPE_NUMBER ||
282+
this.type === TYPE_NUMBER_NAN) return `(${this.source} | 0)`;
283+
return `(+${this.source} | 0)`;
284+
}
285+
264286
asNumberOrNaN () {
265287
if (this.type === TYPE_NUMBER || this.type === TYPE_NUMBER_NAN) return this.source;
266288
return `(+${this.source})`;
@@ -394,7 +416,8 @@ const MATH_CACHE = {
394416
LN10: 'const LN10=Math.LN10;',
395417
pow: 'const pow=Math.pow;',
396418
max: 'const max=Math.max;',
397-
min: 'const min=Math.min;'
419+
min: 'const min=Math.min;',
420+
random: 'const random=runtime.ext_scratch3_operators._random;'
398421
};
399422

400423
class JSGenerator {
@@ -505,12 +528,14 @@ class JSGenerator {
505528
case 'list.get': {
506529
const index = this.descendInput(node.index);
507530
if (environment.supportsNullishCoalescing) {
531+
if (index instanceof ConstantInput && index.isAlwaysNumber()) {
532+
const indexValue = +index.constantValue;
533+
if (!isNaN(indexValue)) return new TypedInput(`(${this.referenceVariable(node.list)}.value[${(indexValue | 0) - 1}] ?? "")`, TYPE_UNKNOWN);
534+
return new TypedInput(`(${this.referenceVariable(node.list)}.value[(${index.asInt()}) - 1] ?? "")`, TYPE_UNKNOWN);
535+
}
508536
if (index.isAlwaysNumberOrNaN()) {
509-
if (!isNaN(index)) {
510-
return new TypedInput(`(${this.referenceVariable(node.list)}.value[${index - 1}] ?? "")`, TYPE_UNKNOWN);
511-
}
512-
return new TypedInput(`(${this.referenceVariable(node.list)}.value[${index.asNumber()} - 1] ?? "")`, TYPE_UNKNOWN);
513-
537+
if (!isNaN(index)) return new TypedInput(`(${this.referenceVariable(node.list)}.value[${(index - 1) | 0}] ?? "")`, TYPE_UNKNOWN);
538+
return new TypedInput(`(${this.referenceVariable(node.list)}.value[${index.asInt()} - 1] ?? "")`, TYPE_UNKNOWN);
514539
}
515540
if (index instanceof ConstantInput && index.constantValue === 'last') {
516541
return new TypedInput(`(${this.referenceVariable(node.list)}.value[${this.referenceVariable(node.list)}.value.length - 1] ?? "")`, TYPE_UNKNOWN);
@@ -564,9 +589,58 @@ class JSGenerator {
564589
this.usedMathFunctions.add('acos');
565590
this.usedMathFunctions.add('RAD_TO_DEG');
566591
return new TypedInput(`(acos(${this.descendInput(node.value).asNumber()})*RAD_TO_DEG)`, TYPE_NUMBER_NAN);
567-
case 'op.add':
592+
case 'op.add': {
568593
// Needs to be marked as NaN because Infinity + -Infinity === NaN
569-
return new TypedInput(`(${this.descendInput(node.left).asNumber()} + ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN);
594+
const left = this.descendInput(node.left);
595+
const right = this.descendInput(node.right);
596+
if (left instanceof ConstantInput && right instanceof ConstantInput && left.isAlwaysNumber() && right.isAlwaysNumber()) {
597+
const value = +left.constantValue + +right.constantValue;
598+
if (!isNaN(value)) return new ConstantInput(value.toString(), TYPE_NUMBER);
599+
600+
return new TypedInput(`(${left.asNumber()} + ${right.asNumber()})`, TYPE_NUMBER);
601+
}
602+
return new TypedInput(`(${left.asNumber()} + ${right.asNumber()})`, TYPE_NUMBER_NAN);
603+
}
604+
case 'op.subtract': {
605+
const left = this.descendInput(node.left);
606+
const right = this.descendInput(node.right);
607+
if (left instanceof ConstantInput && right instanceof ConstantInput && left.isAlwaysNumber() && right.isAlwaysNumber()) {
608+
const value = +left.constantValue - +right.constantValue;
609+
if (!isNaN(value)) return new ConstantInput(value.toString(), TYPE_NUMBER);
610+
611+
return new TypedInput(`(${left.asNumber()} - ${right.asNumber()})`, TYPE_NUMBER);
612+
}
613+
// Needs to be marked as NaN because Infinity - Infinity === NaN
614+
return new TypedInput(`(${left.asNumber()} - ${right.asNumber()})`, TYPE_NUMBER_NAN);
615+
}
616+
case 'op.multiply': {
617+
const left = this.descendInput(node.left);
618+
const right = this.descendInput(node.right);
619+
if (right instanceof ConstantInput && right.isAlwaysNumber() && +right.constantValue !== 0) {
620+
if (left instanceof ConstantInput && left.isAlwaysNumber()) {
621+
const value = +left.constantValue * +right.constantValue;
622+
if (!isNaN(value)) return new ConstantInput(value.toString(), TYPE_NUMBER);
623+
624+
return new TypedInput(`(${left.asNumber()} * ${right.asNumber()})`, TYPE_NUMBER);
625+
}
626+
}
627+
// Needs to be marked as NaN because Infinity * 0 === NaN
628+
return new TypedInput(`(${left.asNumber()} * ${right.asNumber()})`, TYPE_NUMBER_NAN);
629+
}
630+
case 'op.divide': {
631+
// Needs to be marked as NaN because 0 / 0 === NaN
632+
const left = this.descendInput(node.left);
633+
const right = this.descendInput(node.right);
634+
if (right instanceof ConstantInput && right.isAlwaysNumber() && +right.constantValue !== 0) {
635+
if (left instanceof ConstantInput && left.isAlwaysNumber()) {
636+
const value = +left.constantValue / +right.constantValue;
637+
// at this point we know it cant be NaN because right is nonzero
638+
return new ConstantInput(value.toString(), TYPE_NUMBER);
639+
}
640+
return new TypedInput(`(${left.asNumber()} / ${right.asNumber()})`, TYPE_NUMBER);
641+
}
642+
return new TypedInput(`(${left.asNumber()} / ${right.asNumber()})`, TYPE_NUMBER_NAN);
643+
}
570644
case 'op.and':
571645
return new TypedInput(`(${this.descendInput(node.left).asBoolean()} && ${this.descendInput(node.right).asBoolean()})`, TYPE_BOOLEAN);
572646
case 'op.asin':
@@ -588,9 +662,6 @@ class JSGenerator {
588662
this.usedMathFunctions.add('round');
589663
this.usedMathFunctions.add('PI');
590664
return new TypedInput(`(round(cos((PI * ${this.descendInput(node.value).asNumber()}) / 180) * 1e10) / 1e10)`, TYPE_NUMBER_NAN);
591-
case 'op.divide':
592-
// Needs to be marked as NaN because 0 / 0 === NaN
593-
return new TypedInput(`(${this.descendInput(node.left).asNumber()} / ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN);
594665
case 'op.equals': {
595666
let left = this.descendInput(node.left);
596667
let right = this.descendInput(node.right);
@@ -604,6 +675,10 @@ class JSGenerator {
604675
const rightAlwaysNumber = right.isAlwaysNumber();
605676
// When both operands are known to be numbers, we can use ===
606677
if (leftAlwaysNumber && rightAlwaysNumber) {
678+
if (left instanceof ConstantInput && right instanceof ConstantInput) {
679+
const value = +left.constantValue === +right.constantValue;
680+
if (!isNaN(value)) return new TypedInput(value.toString(), TYPE_BOOLEAN);
681+
}
607682
return new TypedInput(`(${left.asNumber()} === ${right.asNumber()})`, TYPE_BOOLEAN);
608683
}
609684
// In certain conditions, we can use === when one of the operands is known to be a safe number.
@@ -688,9 +763,6 @@ class JSGenerator {
688763
return new TypedInput('PI', TYPE_NUMBER);
689764
case 'op.newline':
690765
return new TypedInput('"\n"', TYPE_STRING);
691-
case 'op.multiply':
692-
// Needs to be marked as NaN because Infinity * 0 === NaN
693-
return new TypedInput(`(${this.descendInput(node.left).asNumber()} * ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN);
694766
case 'op.not':
695767
return new TypedInput(`!${this.descendInput(node.operand).asBoolean()}`, TYPE_BOOLEAN);
696768
case 'op.or':
@@ -703,7 +775,8 @@ class JSGenerator {
703775
if (node.useFloats) {
704776
return new TypedInput(`randomFloat(${this.descendInput(node.low).asNumber()}, ${this.descendInput(node.high).asNumber()})`, TYPE_NUMBER_NAN);
705777
}
706-
return new TypedInput(`runtime.ext_scratch3_operators._random(${this.descendInput(node.low).asUnknown()}, ${this.descendInput(node.high).asUnknown()})`, TYPE_NUMBER_NAN);
778+
this.usedMathFunctions.add('random');
779+
return new TypedInput(`random(${this.descendInput(node.low).asUnknown()}, ${this.descendInput(node.high).asUnknown()})`, TYPE_NUMBER_NAN);
707780
case 'op.round':
708781
this.usedMathFunctions.add('round');
709782
return new TypedInput(`round(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER);
@@ -715,10 +788,11 @@ class JSGenerator {
715788
case 'op.sqrt':
716789
// Needs to be marked as NaN because Math.sqrt(-1) === NaN
717790
this.usedMathFunctions.add('sqrt');
791+
if (node.value instanceof ConstantInput && node.value.isAlwaysNumber()) {
792+
if (+node.value.constantValue < 0) return new TypedInput('0', TYPE_NUMBER);
793+
return new TypedInput(Math.sqrt(+node.value.constantValue).toString(), TYPE_NUMBER);
794+
}
718795
return new TypedInput(`sqrt(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN);
719-
case 'op.subtract':
720-
// Needs to be marked as NaN because Infinity - Infinity === NaN
721-
return new TypedInput(`(${this.descendInput(node.left).asNumber()} - ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN);
722796
case 'op.tan':
723797
return new TypedInput(`tan(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN);
724798
case 'op.10^':

0 commit comments

Comments
 (0)