Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/compiler/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,33 @@ const InputType = {
/** Anything that can be interperated as a number. Equal to NUMBER | STRING_NUM | BOOLEAN */
NUMBER_INTERPRETABLE: 0x12FF,

/** Any string which as a non-NaN neumeric interpretation, excluding ''. */
/** Any string which has a non-NaN neumeric interpretation, excluding ''. */
STRING_NUM: 0x200,
/** Any string which has no non-NaN neumeric interpretation, including ''. */
STRING_NAN: 0x400,
/** Either of the strings 'true' or 'false'. */
STRING_BOOLEAN: 0x800,

/** Any string. Equal to STRING_NUM | STRING_NAN | STRING_BOOLEAN */
STRING: 0xE00,
/** Any string which contains lower case characters */
STRING_HAS_CASE_LOWER: 0x4000,
/** Any string which contains upper case characters */
STRING_HAS_CASE_UPPER: 0x8000,
/** Any string which contains case invarient characters */
STRING_HAS_CASE_INVARIENT: 0x10000,
/* A string which could have any case.
* Equal to STRING_HAS_CASE_LOWER | STRING_HAS_CASE_UPPER | STRING_HAS_CASE_INVARIENT */
STRING_ANY_CASE: 0x1C000,

/** Any string. Equal to STRING_NUM | STRING_NAN | STRING_BOOLEAN | STRING_ANY_CASE */
STRING: 0x1CE00,

/** Any boolean. */
BOOLEAN: 0x1000,
/** Any input that can be interperated as a boolean. Equal to BOOLEAN | STRING_BOOLEAN */
BOOLEAN_INTERPRETABLE: 0x1800,

/** Any value type (a type a scratch variable can hold). Equal to NUMBER_OR_NAN | STRING | BOOLEAN */
ANY: 0x1FFF,
ANY: 0x1DFFF,

/** An array of values in the form [R, G, B] */
COLOR: 0x2000
Expand Down Expand Up @@ -200,6 +210,8 @@ const InputOpcode = {
CAST_STRING: 'cast.toString',
CAST_BOOLEAN: 'cast.toBoolean',
CAST_COLOR: 'cast.toColor',
CAST_UPPER_CASE: 'cast.toUpperCase',
CAST_LOWER_CASE: 'cast.toLowerCase',

COMPATIBILITY_LAYER: 'compat',
OLD_COMPILER_COMPATIBILITY_LAYER: 'oldCompiler',
Expand Down
103 changes: 100 additions & 3 deletions src/compiler/intermediate.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,56 @@ class IntermediateStackBlock {
*/
class IntermediateInput {

/**
* @param {string} constant
* @param {boolean} preserveStrings
* @returns {InputType}
*/
static getInputType (constant, preserveStrings = false) {
const numConstant = +constant;

const getCaseFlags = () => {
let stringCaseFlags = 0;

for (let i = 0; i < constant.length; i++) {
const char = constant.charAt(i);
const charUpper = char.toUpperCase();
const charLower = char.toLowerCase();

if (charUpper === charLower) {
stringCaseFlags |= InputType.STRING_HAS_CASE_INVARIENT;
} else if (char === charLower) {
stringCaseFlags |= InputType.STRING_HAS_CASE_LOWER;
} else {
stringCaseFlags |= InputType.STRING_HAS_CASE_UPPER;
}
}

return stringCaseFlags;
};

if (!Number.isNaN(numConstant) && (constant.trim() !== '' || constant.includes('\t'))) {
if (!preserveStrings && numConstant.toString() === constant) {
return IntermediateInput.getNumberInputType(numConstant);
}
return InputType.STRING_NUM | getCaseFlags();
}

if (!preserveStrings) {
if (constant === 'true') {
return InputType.STRING_BOOLEAN | getCaseFlags();
} else if (constant === 'false') {
return InputType.STRING_BOOLEAN | getCaseFlags();
}
}

return InputType.STRING_NAN | getCaseFlags();
}

/**
* @param {number} number
* @returns {InputType}
*/
static getNumberInputType (number) {
if (typeof number !== 'number') throw new Error('Expected a number.');
if (number === Infinity) return InputType.NUMBER_POS_INF;
Expand Down Expand Up @@ -114,10 +164,17 @@ class IntermediateInput {
/**
* Is the type of this input guaranteed to always be the type at runtime.
* @param {InputType} type
* @param {boolean} ignoreCase Should the case of strings be ignored when checking type
* @returns {boolean}
*/
isAlwaysType (type) {
return (this.type & type) === this.type;
isAlwaysType (type, ignoreCase = true) {
let ignore = 0;

if (ignoreCase) {
ignore = InputType.STRING_ANY_CASE;
}

return (this.type & ~ignore & type) === (this.type & ~ignore);
}

/**
Expand Down Expand Up @@ -195,7 +252,7 @@ class IntermediateInput {
}
case InputOpcode.CAST_STRING:
this.inputs.value += '';
this.type = InputType.STRING;
this.type = IntermediateInput.getInputType(this.inputs.value, true);
break;
case InputOpcode.CAST_COLOR:
this.inputs.value = Cast.toRgbColorList(this.inputs.value);
Expand All @@ -207,6 +264,46 @@ class IntermediateInput {

return new IntermediateInput(castOpcode, targetType, {target: this});
}

/**
* When upper is true, returns a string casted to upper case.
* When upper is false, returns a string casted to lower case.
* @param {boolean} upper
* @returns
*/
toStringWithCase (upper) {
const stringified = this.toType(InputType.STRING);

if (upper) {
if (stringified.isSometimesType(InputType.STRING_HAS_CASE_LOWER)) {
if (stringified.opcode === InputOpcode.CONSTANT) {
// Do the case conversion at compile time
stringified.inputs.value = stringified.inputs.value.toUpperCase();
stringified.type &= ~InputType.STRING_HAS_CASE_LOWER;
} else {
return new IntermediateInput(
InputOpcode.CAST_UPPER_CASE,
stringified.type & ~InputType.STRING_HAS_CASE_LOWER,
{target: stringified}
);
}
}
} else if (stringified.isSometimesType(InputType.STRING_HAS_CASE_UPPER)) {
if (stringified.opcode === InputOpcode.CONSTANT) {
// Do the case conversion at compile time
stringified.inputs.value = stringified.inputs.value.toLowerCase();
stringified.type &= ~InputType.STRING_HAS_CASE_UPPER;
} else {
return new IntermediateInput(
InputOpcode.CAST_LOWER_CASE,
stringified.type & ~InputType.STRING_HAS_CASE_UPPER,
{target: stringified}
);
}
}

return stringified;
}
}


Expand Down
20 changes: 6 additions & 14 deletions src/compiler/irgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,25 +151,17 @@ class ScriptTreeGenerator {
if (constant === null) throw new Error('IR: Constant cannot have a null value.');

constant += '';
const numConstant = +constant;
const preserve = preserveStrings && this.namesOfCostumesAndSounds.has(constant);

if (!Number.isNaN(numConstant) && (constant.trim() !== '' || constant.includes('\t'))) {
if (!preserve && numConstant.toString() === constant) {
return new IntermediateInput(InputOpcode.CONSTANT, IntermediateInput.getNumberInputType(numConstant), {value: numConstant});
}
return new IntermediateInput(InputOpcode.CONSTANT, InputType.STRING_NUM, {value: constant});
}
const constantType = IntermediateInput.getInputType(constant, preserve);

if (!preserve) {
if (constant === 'true') {
return new IntermediateInput(InputOpcode.CONSTANT, InputType.STRING_BOOLEAN, {value: constant});
} else if (constant === 'false') {
return new IntermediateInput(InputOpcode.CONSTANT, InputType.STRING_BOOLEAN, {value: constant});
}
if ((constantType & InputType.NUMBER_OR_NAN) === constantType) {
// If the constant can always be safely treated as number, we turn it into a number here
return new IntermediateInput(InputOpcode.CONSTANT, constantType, {value: +constant});
}

return new IntermediateInput(InputOpcode.CONSTANT, InputType.STRING_NAN, {value: constant});
// Otherwise, return it as a string
return new IntermediateInput(InputOpcode.CONSTANT, constantType, {value: constant});
}

/**
Expand Down
64 changes: 59 additions & 5 deletions src/compiler/jsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const functionNameVariablePool = new VariablePool('fun');
*/
const generatorNameVariablePool = new VariablePool('gen');

/**
* @param {IntermediateInput} input
* @param {IntermediateInput} other
* @returns {boolean}
*/
const isSafeInputForEqualsOptimization = (input, other) => {
// Only optimize constants
if (input.opcode !== InputOpcode.CONSTANT) return false;
Expand All @@ -69,6 +74,46 @@ const isSafeInputForEqualsOptimization = (input, other) => {
return false;
};

/**
* @param {IntermediateInput} left
* @param {IntermediateInput} right
* @param {boolean} forceLower
* @returns {{left: IntermediateInput, right: IntermediateInput}}
*/
const inputsToComperableStrings = (left, right, forceLower) => {

const leftStringified = left.toType(InputType.STRING);
const rightStringified = right.toType(InputType.STRING);

const leftCaseType = leftStringified.type & (InputType.STRING_HAS_CASE_UPPER | InputType.STRING_HAS_CASE_LOWER);
const rightCaseType = rightStringified.type & (InputType.STRING_HAS_CASE_UPPER | InputType.STRING_HAS_CASE_LOWER);

if (leftCaseType === InputType.STRING_HAS_CASE_LOWER || leftCaseType === 0) {
// left only has lower case characters or is invarient, cast right to lower case
return {left: leftStringified, right: rightStringified.toStringWithCase(false)};
}

if (rightCaseType === InputType.STRING_HAS_CASE_LOWER || rightCaseType === 0) {
// right only has lower case characters or is invarient, cast left to lower case
return {left: leftStringified.toStringWithCase(false), right: rightStringified};
}

if (!forceLower) {
if (leftCaseType === InputType.STRING_HAS_CASE_UPPER) {
// left only has upper case characters, cast right to upper case
return {left: leftStringified, right: rightStringified.toStringWithCase(true)};
}

if (rightCaseType === InputType.STRING_HAS_CASE_UPPER) {
// right only has upper case characters, cast left to upper case
return {left: leftStringified.toStringWithCase(true), right: rightStringified};
}
}

// Both strings could be a mix of cases, so we have to cast both.
return {left: leftStringified.toStringWithCase(false), right: rightStringified.toStringWithCase(false)};
};

/**
* A frame contains some information about the current substack being compiled.
*/
Expand Down Expand Up @@ -196,6 +241,10 @@ class JSGenerator {
return `("" + ${this.descendInput(node.target)})`;
case InputOpcode.CAST_COLOR:
return `colorToList(${this.descendInput(node.target)})`;
case InputOpcode.CAST_UPPER_CASE:
return `(${this.descendInput(node.target.toType(InputType.STRING))}.toUpperCase())`;
case InputOpcode.CAST_LOWER_CASE:
return `(${this.descendInput(node.target.toType(InputType.STRING))}.toLowerCase())`;

case InputOpcode.COMPATIBILITY_LAYER:
// Compatibility layer inputs never use flags.
Expand Down Expand Up @@ -287,8 +336,10 @@ class JSGenerator {
return `((Math.atan(${this.descendInput(node.value)}) * 180) / Math.PI)`;
case InputOpcode.OP_CEILING:
return `Math.ceil(${this.descendInput(node.value)})`;
case InputOpcode.OP_CONTAINS:
return `(${this.descendInput(node.string)}.toLowerCase().indexOf(${this.descendInput(node.contains)}.toLowerCase()) !== -1)`;
case InputOpcode.OP_CONTAINS: {
const sameCaseInputs = inputsToComperableStrings(node.string, node.contains, false);
return `(${this.descendInput(sameCaseInputs.left)}.indexOf(${this.descendInput(sameCaseInputs.right)}) !== -1)`;
}
case InputOpcode.OP_COS:
return `(Math.round(Math.cos((Math.PI * ${this.descendInput(node.value)}) / 180) * 1e10) / 1e10)`;
case InputOpcode.OP_DIVIDE:
Expand All @@ -307,7 +358,8 @@ class JSGenerator {
}
// When either operand is known to never be a number, only use string comparison to avoid all number parsing.
if (!left.isSometimesType(InputType.NUMBER_INTERPRETABLE) || !right.isSometimesType(InputType.NUMBER_INTERPRETABLE)) {
return `(${this.descendInput(left.toType(InputType.STRING))}.toLowerCase() === ${this.descendInput(right.toType(InputType.STRING))}.toLowerCase())`;
const sameCaseInputs = inputsToComperableStrings(left, right, false);
return `(${this.descendInput(sameCaseInputs.left)} === ${this.descendInput(sameCaseInputs.right)})`;
}
// No compile-time optimizations possible - use fallback method.
return `compareEqual(${this.descendInput(left)}, ${this.descendInput(right)})`;
Expand All @@ -329,7 +381,8 @@ class JSGenerator {
}
// When either operand is known to never be a number, avoid all number parsing.
if (!left.isSometimesType(InputType.NUMBER_INTERPRETABLE) || !right.isSometimesType(InputType.NUMBER_INTERPRETABLE)) {
return `(${this.descendInput(left.toType(InputType.STRING))}.toLowerCase() > ${this.descendInput(right.toType(InputType.STRING))}.toLowerCase())`;
const sameCaseInputs = inputsToComperableStrings(left, right, true);
return `(${this.descendInput(sameCaseInputs.left)} > ${this.descendInput(sameCaseInputs.right)})`;
}
// No compile-time optimizations possible - use fallback method.
return `compareGreaterThan(${this.descendInput(left)}, ${this.descendInput(right)})`;
Expand All @@ -351,7 +404,8 @@ class JSGenerator {
}
// When either operand is known to never be a number, avoid all number parsing.
if (!left.isSometimesType(InputType.NUMBER_INTERPRETABLE) || !right.isSometimesType(InputType.NUMBER_INTERPRETABLE)) {
return `(${this.descendInput(left.toType(InputType.STRING))}.toLowerCase() < ${this.descendInput(right.toType(InputType.STRING))}.toLowerCase())`;
const sameCaseInputs = inputsToComperableStrings(left, right, true);
return `(${this.descendInput(sameCaseInputs.left)} < ${this.descendInput(sameCaseInputs.right)})`;
}
// No compile-time optimizations possible - use fallback method.
return `compareLessThan(${this.descendInput(left)}, ${this.descendInput(right)})`;
Expand Down
18 changes: 9 additions & 9 deletions test/snapshot/__snapshots__/tw-NaN.sb3.tw-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,52 @@
const b0 = runtime.getOpcodeFunction("looks_say");
return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 21",}, b0, false, false, "B", null);
if (!(("" + (0 / 0)).toLowerCase() === "0".toLowerCase())) {
if (!((("" + (0 / 0)).toLowerCase()) === "0")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "aA", null);
}
if ((("" + (0 * Infinity)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + (0 * Infinity)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "/", null);
}
if (((((0 * Infinity) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "?", null);
}
if ((("" + ((Math.acos(1.01) * 180) / Math.PI)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + ((Math.acos(1.01) * 180) / Math.PI)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "=", null);
}
if ((((((Math.acos(1.01) * 180) / Math.PI) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "]", null);
}
if ((("" + ((Math.asin(1.01) * 180) / Math.PI)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + ((Math.asin(1.01) * 180) / Math.PI)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "_", null);
}
if ((((((Math.asin(1.01) * 180) / Math.PI) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "{", null);
}
if ((("" + (0 / 0)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + (0 / 0)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "}", null);
}
if (((((0 / 0) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "aa", null);
}
if ((("" + Math.sqrt(-1)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + Math.sqrt(-1)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ac", null);
}
if ((((Math.sqrt(-1) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ae", null);
}
if ((("" + mod(0, 0)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + mod(0, 0)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ag", null);
}
if ((((mod(0, 0) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ai", null);
}
if ((("" + Math.log(-1)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + Math.log(-1)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ak", null);
}
if ((((Math.log(-1) || 0) * 1) === 0)) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "am", null);
}
if ((("" + (Math.log(-1) / Math.LN10)).toLowerCase() === "NaN".toLowerCase())) {
if (((("" + (Math.log(-1) / Math.LN10)).toLowerCase()) === "nan")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "ao", null);
}
if (((((Math.log(-1) / Math.LN10) || 0) * 1) === 0)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ return function* genXYZ () {
yield* executeInCompatibilityLayer({"MESSAGE":"plan 1",}, b0, false, false, "j", null);
b1.value = (1 + 2);
yield* thread.procedures["Znon-warp recursion %s"](2);
if ((("" + listGet(b2.value, b1.value)).toLowerCase() === "the only thing".toLowerCase())) {
if (((("" + listGet(b2.value, b1.value)).toLowerCase()) === "the only thing")) {
yield* executeInCompatibilityLayer({"MESSAGE":"pass",}, b0, false, false, "t", null);
}
yield* executeInCompatibilityLayer({"MESSAGE":"end",}, b0, false, false, "s", null);
Expand Down
Loading